Test Doubles

The other day I broke our tests by including a Module that required configuration. I didn’t realize this until our Continuous Integration setup reported failures. If I wanted to get my pull request through I would need those tests to pass. One dev recommended a test adapter. I began designing and thinking of a solution to get this module working while testing.

However, another dev had a much simpler solution and resolved it with a single line in the spec:

allow(Mail::NewUser).to receive(:send)

RSpec refers to this as a Test Double, after Martin Fowlers article. The idea is that our tests are specific to a single value or behavior. However, these can unexpectedly change when they depend on another object’s value or behavior. If we can replace that dependency with an object providing consistent behavior we can keep our tests simple and clean.

Suppose I want to test a Hello module. It accepts an instance of Person and says hello.

module Hello
  def Hello.person(person)
    "Hello #{person.name}"
  end
end

An issue with testing this module is that the Person class is complex. While person.name is expected to be a short string, it’s a complicated algorithm to generate and create.

This shouldn’t be an interruption to creating the Hello test. It’s also not the time to think about the Person class. How can we test the Hello module’s behavior without having an instance of Person readily available?

Below I have created two tests. The first uses an Open Struct to provide the needed behavior to test Hello.person. The second uses RSpec’s doubles to create an instance of Person. It also has a ‘name’ method to call and return the value ‘Dave’.

RSpec.describe "Hello" do
  it "accepts a dummy object to greet" do
    require 'ostruct'
    dave = OpenStruct.new(name: 'Dave')
    expect(Hello.person(dave)).to eq("Hello Dave")
  end

  it "uses a test double to greet" do
    dave = double("Person", name: "Dave")
    expect(Hello.person(dave)).to eq("Hello Dave")
  end
end

These tests are only concerned with the Hello Module. They do it with minimal knowledge of what the Person class is. Whatever happens to Person, this test double will always remain the same, leaving our tests safe and happy.

Test doubles are beneficial.

  • They enable you to be lazy. In our example we didn’t need to know the Person object to test Hello.
  • They prevent things from creeping into our tests that we are not testing.

I hope this overview of Test Doubles helped you understand testing. I recommend the RSpec Mock documents for a full overview of RSpec and test doubles.

http://www.relishapp.com/rspec/rspec-mocks/v/3-5/docs

Uncle Bob has also written a helpful article on the strengths and weaknesses of mocks, https://8thlight.com/blog/uncle-bob/2014/05/10/WhenToMock.html.

Leave a Reply

Your email address will not be published. Required fields are marked *