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.

Ruby’s Lonely Operator or Safe Navigation Operator

Poking through some Pull Requests I spotted syntax I’ve never seen before and found difficult to google:

person&.job&.company&.id

The “&.” was introduced in Ruby 2.3.0 as the Lonely Operator. It prevents the error “NoMethod error on Nil”. If the object is nil it just returns nil. Otherwise it continues down the chain and gives the intended result.

This allows us to skip the heavy handed code of

person && person.job && person.job.company && person.job.company.id

Using the Lonely Operator looks better. It does have valid criticisms against it. First, it doesn’t address the original issue: having a null object when you weren’t expecting it. A reliable and recommended solution is the Null Object Pattern.  An author by the name Franzejr has a clear Ruby implementation of the Null Object Pattern.

Second, this encourages us to violate the Law of Demeter.  The problem of getting a persons company id is the tight coupling between those objects. This increases the cost of maintenance and chances for technical debt.

I’m curios to see where the Lonely Operator goes. I’m happy to have it in my tool belt. It simplifies chaining multiple methods. It accommodates legacy code where the Null Object Pattern may complicate a code base further.

While researching Ruby’s Lonely Operator I found the following articles helpful:

Let me know what you think in the comments. I’m curios how often you find the Lonely Operator in the wild and if you use it.