RSpec Tips: allow_any_instance_of

Alejandro Vélez-Calderón
Beam Benefits
Published in
3 min readJun 2, 2021
Photo by Diego PH on Unsplash

Any system under test needs some degree of mocks. Boundaries need to be drawn in order to accentuate each layer of your testing pyramid. By mocking out side effects we are able to test the interface of a single unit (method/worker/service etc.) and ensure that many edge cases can run quickly and consistently.

If you are an RSpec user the way to accomplish these is to use allow in combination with an instance_double or class_double.

cool_class.rb

class CoolClass
def self.foo
'bar'
end
end

cool_class_spec.rb

require 'rspec'

RSpec.describe CoolClass do
let
(:mocked_cool_class) { class_double(CoolClass) }

it 'returns cool! because we kindly asked it to' do
allow(mocked_cool_class).to receive(:foo).and_return('cool!')
expect(mocked_cool_class.foo).to eq('cool!')
end
end

Verifying doubles can be tedious to setup in cases where you don’t have a clear picture of how your unit under test is using your mocked class. And what if we don’t care? What if the mocked class is a side effect of which you have no concern about? It is still useful to make sure you are calling the side effect, but can we assure it is being called with minimal work?

In this case, you might be tempted to reach for any_instance_of which appropriately mocks out any instance of the class.

Let’s say someone has a need for a side effect to execute within your class.

contrived_service.rb

class ContrivedService
def some_side_effect
# ... some complicated business process
end
end

cool_class.rb

require 'contrived_service'class CoolClass

def
cool_method
ContrivedService.new.some_side_effect
'Hiya buddy!'
end

def self
.foo
'bar'
end
end

But all we care about is that cool_method returns ‘Hiya buddy!’ and we don’t have a clear picture of what some_side_effect does.

Let’s just mock any instance of ContrivedService! 😍

cool_class_spec.rb

require 'rspec'

RSpec.describe CoolClass do
# ...
describe
'#cool_method' do

before do
allow_any_instance_of(ContrivedService)
.to receive(:some_side_effect)
end

it
'returns hiya buddy and does something else that we are not worried about right meow' do
expect_any_instance_of(ContrivedService)
.to receive(:some_side_effect)
expect(described_class.new.cool_method).to eq('Hiya buddy!')
end
end
end

Boom! Tests are passing, we are accurately testing the interface of our cool_methodand it’s time for a coffee break!

Some time passes, and a stakeholder realizes we should’ve been calling our side effect multiple times in certain scenarios! Ok no problem we know exactly how to do that!

def cool_method(cool_argument = 'no')
ContrivedService.new.some_side_effect
ContrivedService.new.some_side_effect if cool_argument == 'yes'
'Hiya buddy!'
end
...context 'when passed in yes' do
it
'runs the side effect twice' do
expect_any_instance_of(ContrivedService)
.to receive(:some_side_effect).twice
expect(described_class.new.cool_method('yes')).to eq('Hiya buddy!')
end
end
end

But wait, our tests fail, RSpec says:

RSpec::Mocks::MockExpectationError: The message 'some_side_effect' was received by #<ContrivedService:103980 > but has already been received by #<ContrivedService:0x00007fbf2cb887e0>

any_instance_of with a numerical qualifier asserts that any one instance of that class received the method n number of times. Additionally, our double is scoped to one instance and one instance only. This means additional instances of ContrivedService have undefined behavior so RSpec appropriately raises an error.

In this case, we should reach for more granular doubles instead, with a bit more work we can make our tests accurately reflect what should be happening.

describe '#cool_method' do
let
(:instance) { instance_double(ContrivedService) }
let(:double_class) {
class_double(ContrivedService).as_stubbed_const
}

before do
allow(double_class).to receive(:new).and_return(instance)
allow(instance).to receive(:some_side_effect)
end

it
'returns hiya buddy and does something else that we are not worried about right meow' do
expect(described_class.new.cool_method).to eq('Hiya buddy!')
expect(instance).to have_received(:some_side_effect)
end

context
'when passed in yes' do
it
'runs the side effect twice' do
expect(described_class.new.cool_method('yes')).to eq('Hiya buddy!')
expect(instance).to have_received(:some_side_effect).twice
end
end
end

Now we are shoveling all new instances of our service through the same double, so RSpec gives us the ✅ .

This additional granularity allows future devs to change what the service class does without fear of impacting other tests. Let’s say our side effect needs some safeguarding because it can throw a NoMethodError in some cases.

def cool_method(cool_argument = 'no')
begin
ContrivedService.new.some_side_effect
ContrivedService.new.some_side_effect if cool_argument == 'yes'
'Hiya buddy!'
rescue NoMethodError
'Sorry buddy something happened!'
end
end
....context 'when the side effect blows up' do
before do
allow(instance).to receive(:some_side_effect).and_raise(NoMethodError)
end

it
'returns an appropriate response' do
expect(described_class.new.cool_method).to eq('Sorry buddy something happened!')
end
end

Any system under test inevitably needs to handle tradeoffs.

Although we’d like to say we never use any_instance_of there are some cases where it may make sense!

It’s important to understand the pros and cons of our tools so that we are appropriately suited to update them in the future.

--

--

Alejandro Vélez-Calderón
Beam Benefits

Senior Software Engineer @ Curology from Bayamón, Puerto Rico