Isolated Unit Testing with Test Doubles

A painfully simple example in Ruby with rspec and rspec-mocks

When practicing Test-Driven Development, it’s important to ensure that your unit tests actually test just a single unit (often a single class) of your codebase. If your unit tests depend too heavily on external classes, it becomes difficult to determine the source of a test failure. One common way to achieve isolation is to implement a test double rather than rely on the inner workings of an external class.

Like a stunt double, a test double will jump into action and replace an actual object inside of your test. Test doubles help isolate your unit tests, ensuring that the tests you write actually test a single unit. They can also help speed up your tests by avoiding costly or slow processes, such as emitting a request to an actual API (which you may or may not own) or querying a database (which may contain production data or need to be seeded).

The method or class being tested is often referred to as the System Under Test, or SUT. In a strongly-typed language, a test double must conform to the interface of an object on which the SUT depends. By conforming to the object’s interface, the SUT has no idea that it has been passed in an object other than the object that it would typically receive in production. In a dynamic language like Ruby, doubles act as duck types, responding to a method call that exists in the implementation of the system it stands in for. In this example, we’ll focus on the latter type using Ruby and RSpec.

This article assumes that you are already familiar with unit testing (specifically with RSpec). If you don’t already feel comfortable with those topics, here are some suggested readings before returning to mocks.

A Note about Nomenclature

If you look up the difference between test doubles and mocks, you might be surprised to realize that there isn’t a consensus about their difference. Martin Fowler argues that the the former utilizes state verification while the latter uses behavior verification to test a given object. While the distinction might work on a theoretical level, the mocking framework that you choose will often decide the difference for you. For example, rspec-mocks blurs the distinction between the two. As per the RSpec mocks documentation:

rspec-mocks is a test-double framework for rspec with support for method stubs, fakes, and message expectations on generated test-doubles and real objects alike.

The distinct implementations of mocks and doubles among different mocking libraries and frameworks makes it imperative to always read the documentation instead of relying on a preconceived notion about what a mock or a test double actually is.

As with most aspects of software development, passionate developers with strongly-held opinions will often back or admonish certain philosophies about testing. My goal is not to convince you that you should prefer one method of testing over another. Rather, I hope to demonstrate that isolated unit testing isn’t difficult once you’re familiar with the tools available to you.

A Simple, Wholesome Example

In a more perfect world, every human being would have a pet. A human would be able to interact with its pet through predetermined actions: feeding, petting, etc. Those actions would affect the pet’s emotional and physical qualities.

To abstract that behavior into object-oriented code, we can think of a few classes that capture the desired behavior. First, we’d want a Human class, which is initialized with a pet. Because different species of pets vary so much from each other, we’d want to create a hierarchy of classes to represent the different possible pet species. The superclass Pet would contain the functionality common across all pets. For this example, we’ll establish that all pets eat, and they all make some kind of noise. A specific species would be able to further refine the behavior common to all pets, and it might even add its own unique behaviors.

In this example, the Human class will depend on the Pet class (and possibly its inherited classes) because a human instance will invoke methods on a pet instance. In other words, the Human class is our SUT. To demonstrate how the use test of doubles allows us to develop and test a class before its dependency has been created, we’ll develop our Human class as we test it.

Instantiating a Human

We can follow the suggestion of Freemand and Pryce in Growing Object-Oriented Software Guided By Tests and write our tests as if the implementation of the system under test already exists (84). This approach helps us begin to think about good variable and method names. The strategy also helps us begin to see how much setup is required to instantiate and object. A complicated, verbose setup could signal that we can abstract some of the setup steps into a distinct class.

Using RSpec expectations, our first test should simply assert that an instantiated human contains and exposes a pet. We really don’t want to get sidetracked building out the Pet class, but to ensure that our doubles match our eventual implementation, we will be creating instance_doubles rather than a regular old doubles. When using an instance_double, RSpec will throw an error if the object you wish to replace doesn’t contain a method you intend to use.

describe Human do
context "init" do
it "should initialize a Human with a Pet" do
pet = instance_double("Pet")
human = Human.new(pet)
expect(human.pet).to eq(pet)
end
end
end

It should be no surprise that our test fails with an error:

An error occurred while loading ./spec/human_spec.rb.
Failure/Error:
describe Human do
context "init" do
it "should initialize a Human with a Pet" do
pet = instance_double("Pet")
human = Human.new(pet)
expect(human.pet).to eq(pet)
end
end
end
NameError:
uninitialized constant Human
# ./spec/human_spec.rb:6:in `<top (required)>'

Normally I’d proceed by doing the bare minimum to address an error, rerunning the test, and repeating the cycle until I eventually reach a failure. I’ll actually write a bit more code than that to avoid a lot of redundancy in this article. Since every human needs a pet, we’ll ensure that every new human must be created with a pet, and we’ll make that pet accessible via an attr_reader.

class Human 
attr_reader :pet

def initialize(pet)
@pet = pet
end
end

In a strongly-typed language, like Java, you would need to ensure that the test double conforms to the expected interface. Otherwise, your code would not compile, and you’d be unable to run your tests. With Ruby, we’ll need to be a little more verbose inside of our #initialize.

class Human 
attr_reader :pet

def initialize(pet)
raise ArgumentError.new("A Human must be initialized with a Pet") unless pet.is_a?(Pet)

@pet = pet
end
end

Unfortunately, our test double is not an instance of Pet. If you puts(pet.class.name) inside of the test, you’ll see that pet is actually an instance of RSpec::Mocks::InstanceVerifyingDouble. To prevent the error upon initialization, we’ll need to stub the response that we want our double to return. We’ll explore stubs further below, but a stub is simply a hard-coded response to a method call. Below, you can see how we specify the method

context "init" do 
it "should initialize a Human with a Pet" do
pet = instance_double("Pet")
allow(pet).to receive(:is_a?).with(Pet).and_return(true) # STUB CONFIGURATION
human = Human.new(pet)
expect(human.pet).to eq(pet)
end
end

Our single test should now be green! Of course, that error was an expected behavior, so we should definitely create a test for that scenario. To avoid giving the impression that our test double is a type of Pet , we’ll remove the test double and pass in an argument of type String instead.

it "should throw an error if a Human is not initialized with a Pet" do
pet = "Not a pet"
expect { Human.new(pet) }.to raise_error(ArgumentError)
end

Feeding Our Pet

If you own a cat, you’ve probably woken up to loud meows requesting food. With that in mind, the first thing we should program our human to do is feed its furry friend. Like a human, a pet experiences hunger, so feeding it should alter its hunger level.

Even with such a simple example, we’ve quickly hit the boundary of a unit test: an external dependancy. In the real world, the methods that you call will probably not be as simple as the one we will soon implement. They may also change. They might not even work as expected. To save you the headache of figuring out the source of a possible error, you want to ensure that no unit in your unit tests is coupled to the implementation of another.

In this specific case, we can achieve independent tests using a spy.

Spy

As its name implies, a spy spies on an object, tracking the number of calls to an objects’s methods and even the arguments passed into those calls. Depending on the mocking library you are using, a spy may wrap itself around an existing method without altering its underlying behavior. However, rspec-mocks creates spies as_null_objects, so the code you are spying on never runs. Though this behavior may seem intrusive, it ultimately helps up stay focused on developing the current system under test.

As above, we use an instance_spy rather than a regular old spy in order to build up a very simple skeleton of our dependency.

context "feeding a pet" do 
it "should cause the pet to eat" do
pet = instance_spy("Pet")
human = Human.new(pet)

human.feedPet()
expect(pet).to have_received(:eat) # SPY ASSERTION
end
end

Because we haven’t created the #feedPet method, we get the following error:

Failures:
1) Human feeding a pet should cause the pet to eat
Failure/Error: human.feedPet()
NoMethodError:
undefined method `feedPet' for #<Human:0x00007f9780057db8>
# ./spec/human_spec.rb:19:in `block (3 levels) in <top (required)>'

Following our error and going just a little beyond what it requires, we add the following instance method to Human.

def feedPet() 
self.pet.eat()
end

However, that still gives us the expected error:

Failures:
1) Human feeding a pet should cause the pet to eat
Failure/Error: self.pet.eat()
the Pet class does not implement the instance method: eat
# ./lib/human.rb:9:in `feedPet'
# ./spec/human_spec.rb:19:in `block (3 levels) in <top (required)>'

All it takes to get rid of our error now is the following empty instance method to the Pet class:

def eat()
end

If you feel uncomfortable about this empty method, you can always raise an error inside of it. That way, if you accidentally do end up calling the actual method, you’ll know right away. As mentioned above, the pet test double will not invoke the actual method even though it requires the method to exist.

def eat() 
raise "Pet#eat has not been implemented"
end

When we later arrive at Pet's unit tests, we can determine what we actually want the #eat method to do. Perhaps we want to track our pet’s hunger level as an Integer, which goes up by one when it is fed. Perhaps we’d like to track a pet’s weight, which increases if a pet eats while it’s already full. Perhaps the pet is just like my old cat, and her hunger level will remain high despite how much you feed her. Because of our isolated test, we can focus on the human‘s behavior without simultaneously concerning ourselves with the pet‘s behavior or change of state.

Imitating Our Pet

It’s safe to say that every pet owner has imitated the sound that its pet makes, so we’ll test that next. The naive approach to this test might be to assert against a String we expect to receive from our pet. However, there are many reasons for a pet’s sounds to change. Maybe a cat hisses when it’s hungry but purrs when it’s full. Or maybe our human adopts a new species after its old pet runs away.

Stubs

Instead of relying on a value that would very likely change, we’ll make use of a stub. Asserting against a stubbed response helps us conform to the Liskov substitution principle, which states that “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.” In other words, it shouldn’t matter what kind of pet our human interacts with as long as the pet behaves like a pet and exposes the expected methods.

As above, we rely on an instance_double to ensure that we’ve laid out the foundation for our dependent class.

context "imitating a pet" do 
it "should speak the sound that its pet makes" do
pet = instance_double("Pet")
stubbedPetSound = "Oink"
allow(pet).to receive(:speak).and_return(stubbedPetSound)
human = Human.new(pet)
    imitation = human.imitatePet()
expectedImitation = "It's so funny when my pet says #{stubbedPetSound}"
expect(imitation).to eq(expectedImitation)
end
end

By now, we should be pretty familiar with the error we expect to receive.

Failures:
1) Human imitating a pet should speak the sound that its pet makes
Failure/Error: allow(pet).to receive(:speak).and_return(stubbedPetSound)
the Pet class does not implement the instance method: speak
# ./spec/human_spec.rb:29:in `block (3 levels) in <top (required)>'

After creating an empty Pet#speak method, we’ll skip the error we know we’ll receive for not having implemented Human#imitatePet and create that method now.

def imitatePet() 
return "It's so funny when my pet says #{self.pet.speak()}"
end

Stubs are incredibly useful when unit testing an instance method that relies on the returned value of another class’s method. In our example above, we don’t know what kind of sound our pet will make. We don’t even know what kind of pet our owner might have. When those things eventually change inside of the Pet class, our Human test will not have to change as long as the publicly exposed method signature remains the same.


Because our Pet class doesn’t yet have an external dependency, it will not need the rspec-mocks framework to remain isolated. As long as it remains without an external dependency, testing that class should remain relatively straightforward, so I won’t demonstrate its tests.

Hopefully this simple example helps you understand how make use of mocks to isolate your unit test as well as why it’s important to think about isolation.

For further reading:

Mocks Aren’t Stubs, by Martin Fowler

State Verification v. Behavior Verification