Writing tests is important (yes, we all know that) and it’s taken me a couple of years to get familiar with writing tests with RSpec. Learning to write method stubs, fakes, working with test doubles and working around caveats - big and small. For example: not every class is required to test what the usecases or classes within it return. It might be sufficient in these scenarios to get away with rspec-mocks, RSpec’s test double-framework. While first learning about RSpec I often got stuck with the test setup using mocks. There’s a plethora of blogs and Stack Overflow posts about this, these are just some that I didn’t come across as easily and the ones I use repeatedly in testing.
- Using stub/unstub
This is the ‘old’ way of allowing messages but has come in handy in a recent implementation. For example in the loop scenario where the first call on an object needs stubbing, but the second does not to replicate intended behaviour of the loop.
Class Apple
loop do
x = Foo.something
end
break unless Bar.find_by(x: x)
end
let(:foo) { Foo.new }
before do
foo.stub(:something) do
foo.unstub(:something) do
end
end
- Configuring responses with
and_return
and instance_doubles
This is handy when you want to set up your test with some responses. You may have a class that calls another usecase. In this case you can mock the messages to the class and return a mock of intended usecase and even mock the usecase itself. For example:
Class GoodApple
def foo
# use_case returns value of do_something
use_case = Apple.knife
...
end
end
let(:use_case_double) do
instance_double(Apple, do_something: did_something)
end
before do
allow(Apple).to_receive(:knife).and_return(use_case_double)
end
- Another commonly used example is the block implementation where you require a bit more flexibility but also allows you exactly match the method calls. You can mock the arguments and the return values of the block. For example:
Class GoodApple
def foo
use_case = Apple.knife(plate: funky_plate)
...
end
end
let(:plate) { create(:plate) }
let(:use_case_double) do
instance_double(Apple, do_something: did_something)
end
before do
allow(Apple).to_receive(:knife).with(plate: plate).and_return(use_case_double)
end
- Chaining method calls
Passing messages by allowing messages to be received. Similar to the above but here we link the mocks. For example:
Class GoodApple
def foo
use_case = Apple.knife(plate: funky_plate)
return use_case.core.seed
end
end
let(:plate) { create(:plate) }
let(:use_case_double) do
instance_double(Apple, core: core_double)
end
let(:core_double) do
instance_double(CoreClass, seed: '123')
end
before do
allow(Apple).to_receive(:knife).with(plate: plate).and_return(use_case_double)
end
- Open Structs vs Instance Doubles
Both do same thing in terms of faking objects, however I personally find instance_doubles easier to follow. An example of an OpenStruct for the usecase double above could be:
let(:use_case_double) do
OpenStruct.new(core: core_double)
end
comments powered by