Blog

Wednesday, December 21, 2011

An Example using RSpec double, mock, and stub

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.

Please note this blog is no longer maintained. Please visit CivilCode Inc - Custom Software Development.