This is a follow-up post for RSpec double, mock, and stub from earlier this year. A reader asked for an example code to illustrate the various double aliases in RSpec.
For this example we will spec a Transfer class that encapsulates the use case of transferring an amount between two accounts, the source account and destination account. The Transfer is the subject of our examples, while the source and destination accounts are the collaborators.
In the spec below, the first example "should decrease source amount by 10", specifies the interaction for the source account, hence the use of the mock to generate the test double. In this example, the destination account is treated as a secondary collaborator, as we are focusing on the role of the source account. Any object can play the role of the source account, as long as it responds to the #decrease method.
In the second example "should increase destination account by 10", the destination account is the primary collaborator. The use of mock to generate the source account communicates this intent.
describe Transfer do context "transfer with amount of 10" do it "should decrease source account by 10" do source_account = mock('source account') destination_account = stub('destination_account').as_null_object source_account.should_receive('decrease').with(10) transfer = Transfer.new(source_account, destination_account, 10) transfer.call end it "should increase destination account by 10" do source_account = stub('source account').as_null_object destination_account = mock('destination_account') destination_account.should_receive('increase').with(10) transfer = Transfer.new(source_account, destination_account, 10) transfer.call end end end
However, there is some duplication in this spec which can be eliminated by moving the test doubles to let blocks. It's appropriate to use double here to generate the source and destination accounts as both play primary and secondary collaborators in the following examples. Below we stub the methods for each collaborator, then set expectations on those methods for the appropriate examples.
describe Transfer do context "transfer with amount of 10" do let(:source_account) { double('source account', :decrease => nil) } let(:destination_account) { double('destination_account', :increase => nil) } it "should decrease source account by 10" do source_account.should_receive('decrease').with(10) transfer = Transfer.new(source_account, destination_account, 10) transfer.call end it "should increase destination account by 10" do destination_account.should_receive('increase').with(10) transfer = Transfer.new(source_account, destination_account, 10) transfer.call end end end
Update: Radoslav Stankov provides a cleaner refactoring of the spec.
The implementation of the transfer class may look something like this:
class Transfer def initialize(source_account, destination_account, amount) @source_account = source_account @destination_account = destination_account @amount = amount end def call # In a complete implementation, it would make sense to have the following # lines wrapped in a transaction, but I wanted to keep this example simple # @source_account.decrease(amount) @destination_account.increase(amount) end end
It's important to remember that double, mock, and stub all return an instance of RSpec::Mocks::Mock. While the concepts of mocking, (i.e. setting a method expectation), and stubbing are "method-level concepts"[1], the use of mock and stub can communicate which collaborator you are specifying the behaviour for.
[1] For a more complete discussion on this, checkout The RSpec Book.