Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSpec 3 best practices and expect_any_instance_of in Rails

I'm looking for best-practice alternatives to expect_any_instance_of, because the RSpec documentation discourages using expect_any_instance_of:

This feature is sometimes useful when working with legacy code, though in general we discourage its use for a number of reasons: ... [reference]

I use expect_any_instance_of a lot in cases where I want to test that a method will be called when certain conditions are met, but where the object will be loaded in a different scope.

For example, when writing a controller spec, I simply want to test that the correct method is called with the correct parameters on an instance of X.

like image 338
Daniel Avatar asked Aug 31 '17 23:08

Daniel


1 Answers

Ok, well. The answer is - it depends :)

Here are some things, that may help you:

1) Have a look at the way you are testing code. There are (in general) two ways to do it.

Suppose you have this class:

class UserUpdater
  def update(user)
    user.update_attributes(updated: true)
  end
end

Then you can test it in two ways:

Stub everything:

it 'test it' do
  user = double(:user, update_attributes: true)
  expect(user).to receive(:update_attributes).with(updated: true)
  UserUpdater.new.update(user)
end

Minimal (or no) stubbing:

let(:user) { FactoryGirl.create(:user) }
let(:update) { UserUpdater.new.update(user) }

it { expect { update }.to change { user.reload.updated }.to(true) }

I prefer the second way - because it is more natural and gives me much more confidence in my tests.

Back to your example - are you sure that you want to check the method call when the controller action runs? In my opinion - it is better to check the result. Everything behind it should be tested separately - for example, if your controller has a service called - you will test everything about this service in it's own spec, and how the action works in general (some kind of integration tests) in the controller spec.

2. Check what is returned, not how it works:

For example, you have a service, which can find or build a user for you:

class CoolUserFinder
   def initialize(email)
      @email = email
   end

   def find_or_initialize
      find || initialize
   end

   private

   def find
     User.find_by(email: email, role: 'cool_guy')
   end

   def initialize
     user = User.new(email: email)
     user.maybe_cool_guy!

     user
   end
end

And you can test it without stubbing on any instance:

let(:service) { described_class.new(email) }
let(:email) { '[email protected]' }
let(:user) { service.find_or_initialize }

context 'when user does not exist' do
  it { expect(user).to be_a User }
  it { expect(user).to be_new_record }
  it { expect(user.email).to eq '[email protected]' }
  it { expect(user.role).to eq 'maybe_cool_guy' }
  it { expect(user).to be_on_hold }
end

context 'when user already exists' do
  let!(:old_user) { create :user, email: email }

  it { expect(user).to be_a User }
  it { expect(user).not_to be_new_record }
  it { expect(user).to eq old_user }
  it { expect(user.role).to eq 'cool_guy' }
  it { expect(user).not_to be_on_hold }
end

3. And finally sometimes you REALLY need to stub any instance. And it's ok - sometimes shit happens :)

Sometimes you can also replace any_instance with stub like this:

allow(File).to receive(:open).and_return(my_file_double)

I hope it will help you a bit and I hope it is not too long :)

like image 161
unkmas Avatar answered Sep 23 '22 14:09

unkmas