简体   繁体   中英

RSpec: stub methods in iterator

I want to learn how to use stubs.

class SomeClass

attr_reader :current_user

  def initialize(current_user:)
    @current_user = current_user
  end

  def deliver
    subscribers.each do |user|
      DailyEmail.new(recipient: user).deliver
    end

    201
  end

  private

  def subscribers
    User.all.select(&:email_notifications_enabled?)
  end
end

What is the correct way to test DailyEmail new, deliver methods which is called from SomeClass. And how i can test each method if subscribers is activerecord relation? How i check status returns after iterator?

My strange solution:

RSpec.describe SomeClass do

  let(:current_user) { 'user' }
  subject { described_class.new(current_user: current_user) }

  describe '#deliver' do
    let(:subscribers) { ['test2', 'test1'] }

    context 'when `each`, `new`, `deliver` methods called in controller `deliver` method' do
      it 'calls methods' do
        allow(subscribers).to receive(:each)

        subscribers.each do |user|
          the_double = instance_double(DailyEmail)
          expect(DailyEmail).to receive(:new).and_return(the_double).with(recipient: user)
          expect(the_double).to receive(:deliver)
          expect(subscribers).to have_received(:each)
          subject.deliver
        end
      end
    end
  end
end

I wrote something but this implementation seems terrible to me. I don't understand what to do with the iterator and how to test the status. Give a couple of tips please

There are a few things here.

First is that your controller status messages is just wrong. What you do now is returning a 201 body, and not the status. It should return a 200, you're executing a operation which sends an e-mail and not creating an object. If you don't want to return anything else other than a positive error message and not handle error messages (which you should), you should replace 201 with this: render status: 200

Your test is at the moment not really testing anything. If your subscribers method has an error it would not catch it, if your mail class has an error has an error you would not catch it, so what is the point.

For the logic in the controller it self, you should either properly loop trough the mails to be delivered, and check that they're delivered https://relishapp.com/rspec/rspec-rails/docs/mailer-specs or split the test in two. Where you test happy pathing with the controller test and create another test for the email class.

The correct way to run a controller test is to use request spec, and expect the correct response code. Notice here that if you've enabled subscribers in the test db it'll return a 200 and if there are no subscribers it will also just return a 200. The whole code is still tested. If there is an error in the subscriptions method it'll return a 500 error. https://relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec

To properly test the controller test, you need to create the objects in your test db, then loop trough them instead of trying to mock it out like that. You can do this with FactoryBot for example, or you could even stub out the user model, if you for some reason can't add FactoryBot like so; depending on what you're doing in the mailer class.

 before :each do 
   stub_const('User', MockedUserModel)
 end
 
 class MockedUserModel < User
   def all
     arr_of_mocked_users = []
     arr_of_mocked_users << User.new(name: 'mocked_user_1', id: 1)
     arr_of_mocked_users << User.new(name: 'mocked_user_1', id: 2)
     arr_of_mocked_users 
   end

   def email_notifications_enabled?
     true
   end
 end

There are several weird things in your current implementation. Firstly, this:

class SomeController < ApplicationController
  def initialize(current_user:)
    @current_user = current_user
  end
end

...Defining a custom initialize method, purely to make the tests work??!

The standard way to test controllers in modern rails applications is via a request spec . (You can also use controller specs , but this is generally considered inferior as you're bypassing crucial parts of the full stack, such as the router.)

Secondly, these weird variables:

let(:current_user) { 'user' }
let(:subscribers) { ['test2', 'test1'] }

...Why not use real User objects??, Doing it your way makes the testing more complicated, more brittle. and harder to reason with.

Some people might strongly prefer to always decouple these tests from the database, in which case you could set up mocks like:

allow(User).to receive(:all).and_return(subscribers)

Others (including me) would rather just create real objects in the database and accept that your test suite is going to be a little bit slower here.

You don't need to use a library like factory_bot here, but I'd recommend it. Something like this:

let(:current_user) { FactoryBot.create(:user) }
let!(:subscribers) { FactoryBot.create_list(:user, 2, email_notifications_enabled: true) }
let!(:non_subscriber) { FactoryBot.create(:user, email_notifications_enabled: false) }

And finally, I'd get rid of these parts in the test:

allow(subscribers).to receive(:each)
expect(subscribers).to have_received(:each)
subject.deliver

Instead, if your test just runs an end-to-end scenario on the API, and you're establishing real database objects, then you can ensure everything is being tested in a much more comprehensive/realistic manner.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM