The 5 Test Doubles

Test-Driven Development (TDD) helps developers ship code faster, reduce the number of errors and bugs in the application lifecycle, and provide lightning-fast feedback during development.

One element that can be found in every test suite is a test double. The test double is a resource that helps us exercise code that has dependencies when writing tests.

The idea of this writing is to explain the differences between 5 test doubles that are used to exercise tests.

Dummy

A Dummy is just an object (or value) that is required in order to run a method, but the dummy object is not exercised during the test execution. Basically a placeholder, it is commonly used when we need to meet a method signature, but we do not care about its internal workings or behavior in the context of the test.

Let’s imagine that we have a simple class like the following:

class UserNotifier
  def notify(user) = puts "Notified user!"
end

We cannot call the notify method without passing an argument, even though the parameter is not used. Because of this requirement, we have to write the spec like the following:

require 'spec_helper'
require_relative 'user_notifier'

RSpec.describe UserNotifier do
  class User
    attr_accessor :email
    
    def initialize(email)
      @email = email
    end
  end
  
  it 'sends a notification to the user' do
    user = User.new('user@example.com')
    notifier = UserNotifier.new
    expect { notifier.notify(user) }.to output("Notified user!")
  end
end

Note that we created a class just to satisfy the notify method signature. In theory, we could use anything, like an integer, for example:

require 'spec_helper'
require_relative 'user_notifier'

RSpec.describe UserNotifier do
  it 'sends a notification to the user' do
    user = 1 # we can literally use anything in this case
    notifier = UserNotifier.new
    expect { notifier.notify(user) }.to output("Notified user!")
  end
end

Although we can use any value for calling this function, this is not a good practice since it does not express intent, and that will be taken into consideration when another person reads the test.

We should not consider a test only as a confirmation of behavior, but as a living documentation of the exercised code.

Stub

A Stub is used to define a specific return value or behavior for a method on a test double or real object. This is useful when we want to isolate the code being tested and control its dependencies.

Let’s add a small change to the UserNotifier class previously defined:

class UserNotifier
  def notify(user)
    puts "Notified #{user.email}"
  end
end

Considering that we are now using the email of the user passed as an argument, the spec using a Stub would be written as follows:

require 'spec_helper'
require_relative 'user_notifier'

RSpec.describe UserNotifier do
  it 'sends a notification to the user' do
    user = double('User', email: 'user@example.com')
    notifier = UserNotifier.new
    expect { notifier.notify(user) }.to output("Notified user@example.com")
  end
end

As you can see, the use of doubles is pretty straightforward. It is particularly useful when we need to exercise methods that have a dependency that can return different values.

Spy

The use of a Spy is similar to a Stub, but with a different goal: it tracks the calls made to the Spy. This specific behavior enables us to assert the calls made to the Spy. Let’s consider the previous UserNotifier class and use a Spy instead of a Stub:

require 'spec_helper'
require_relative 'user_notifier'

RSpec.describe UserNotifier do
  it 'calls User#email to send the notification' do
    user = spy('User')
    notifier = UserNotifier.new
    notifier.notify(user)
    expect(user).to have_received(:email).once
  end
end

The above example doesn’t exercise the UserNotifier code, but we can observe how many calls and what parameters were used for method calls.

A simple rule of thumb for choosing between Stubs or Spies:

  • You want to control behavior -> Stub;
  • You want to verify behavior -> Spy;

Their use is closely related to the way applications are tested. You can learn further by reading about Classical TDD and “Mockist” TDD.

Mock

Mocks and Spies are very similar: both are used for verifying method calls, but the expectations are written at different moments. If the expectation of the Spy is written after the method call that is being exercised, the Mock expectation is defined upfront:

require 'spec_helper'
require_relative 'user_notifier'

RSpec.describe UserNotifier do
  it 'calls User#email to send the notification' do
    user = double('User')
    notifier = UserNotifier.new
    expect(user).to receive(:email)
    notifier.notify(user)
  end
end

So, if we compare with the Spy example, the only thing that changes in the code is when the expectation is written in the test. Is this the only difference? Absolutely no. The secret lies in the following snippet:

RSpec.describe 'Mock vs Spy' do
  it 'Uses Spy' do
    user = spy('user')
    UserNotifier.new.notify(user)
  end

  it 'Uses Mock' do
    user = double('user')
    UserNotifier.new.notify(user)
  end
end

If you run the above tests, you will notice 2 things: The test using a Mock (the double function) will fail. In the first example, we had to write the expectation before the UserNotifier#notify method. Being explicit is mandatory to make the test green.

The second thing is that the test using Spy passed, even though we did not assert that the email attribute is being called on the user instance.

Here comes the question: why use a spy if it does not fail when I do not write any expectation?

And the answer is… It depends.

In fact, there are scenarios where we do not want to write the expectations up front. A good (and common) scenario is when dealing with a legacy application, and the application is not entirely covered by tests. It is easier to test the application little by little, and since Spies are useful to test side-effects, they become very handy in this scenario, where you want to test a specific output of a specific method call that is being exercised without checking other side-effect calls.

Mocks force you to write expectations in advance, before calling a method, which is an easy thing to do when building a new feature, or when we know the code being tested.

Mocks are used to enforce that something must happen. Spies are used to observe what happened.

Fake

A Fake is like a toy version of real code: it works enough for testing, but it is not the real thing.

Let’s imagine that we need to write a test that will hit a database. If you run the test against a database, your application will open a connection to the database, execute queries against it, receive a response, and then your test assertions will run. What if we could just have a response without the I/O involved?

That’s what In-Memory drivers normally do. They are basically a layer of code used for stateful integration during testing, without hitting a real database. We may say that an In-Memory driver is a Fake, since we still do have the expected results, but without hitting the real database.

Fake is nothing more than code used just for testing.

Conclusion

With that said, I hope this helps clarify the meaning of each of these terms that are used when dealing with testing applications. I may update this post later to include additional test double types, but the five covered here form a solid foundation.