by Adam Brett

Understanding Test Doubles (Mock vs Stub)

There has been a lot of talk lately around the practice of TDD, which has given rise to talk of Classical vs Mockist and of the different types of "mocks" available. There seems to be some confusion in terms, and hopefully this will clear some of it up.

Background

The first thing we need is a little background - when unit testing, which is primarily what people are talking about when discussing TDD, the class you are testing is called the System Under Test (SUT). There are very few classes that operate entirely in isolation. Usually they need other classes or objects in order to function, whether injected via the constructor or passed in as method parameters. These are known as Collaborators. When we talk about Collaborators in the context of unit testing, we are specifically talking about Collaborators for the System Under Test.

Test Doubles

When most people talk about Mocks what they are actually referring to are Test Doubles. A Test Double is simply another object that conforms to the interface of the required Collaborator, and can be passed in in its place. There are several different types of Test Double, of which a Mock is only one. A lot of the confusion arises as "Mocking Frameworks" can often generate numerous different types of Test Double, as well as Mocks. To understand what a Mock actually is, you first have to understand what it isn't.

Dummies

A Dummy is an object that simply implements an Interface, and does nothing else. It's not intended to be used in your tests and will have no effect on the behaviour. An example would be passing an object into a constructor that isn't used in the path you're taking, or a simple object to add to a collection. For example:

private class FooDummy implements Foo
{
    public String bar() { return null; }
}

public class FooCollectionTest
{
    @Test
    public void it_should_maintain_a_count()
    {
        FooCollection sut = new FooCollection();
        sut.add(new FooDummy);
        sut.add(new FooDummy);
        assertEquals(2, sut.count());
    }
}

In this example, the SUT does not care about method bar(), it just cares that it received an object of type Foo, and it doesn't matter that bar() doesn't have a method body, because for this particular test we never call it.

Stubs

Think of a Stub as a step up from a Dummy. It implements a required Interface, but instead of missing method bodies, it usually returns canned responses in order for you to make assertions on the output of the SUT. For Example:

private class FooStub implements Foo
{
    public String bar()
    {
        return "baz";
    }
}

public class FooCollectionTest
{
    @Test
    public void it_should_return_joined_bars()
    {
        FooCollection sut = new FooCollection();
        sut.add(new FooStub);
        sut.add(new FooStub);
        assertEquals("bazbaz", sut.joined());
    }
}

Spies

A Stub could also be used to maintain state, and make assertions, when a Stub does this it is also known as a Test Spy. For example, checking the contents of a parameter, or the total number of times a method is called:

private class ThirdPartyApiSpy implements ThirdPartyApi
{
    public int callCount = 0;

    public boolean hasMore(Response previousResponse)
    {
        if (this.callCount == 0) {
            return true;
        }
        return false;
    }

    public Response get(int page)
    {
        this.callCount++;
        return new DummyResponse;
    }
}

public class ApiConsumerTest
{
    @Test
    public void it_should_get_all_pages()
    {
        ThirdPartyApiSpy spy = new ThirdPartyApiSpy
        ApiConsumer sut = new ApiConsumer(spy);
        sut.fetchAll()
        assertEquals(2, spy.callCount);
    }
}

Fakes

Similar to a Stub being a step up from a Dummy, the simplest way to think of a Fake is as a step up from a Stub. This means not only does it return values, but it also works just as a real Collaborator would. Usually a Fake has some sort of shortcut that means that they are unsuitable for production. A good example of this would be an InMemoryRepository object, with a "real" counterpart that persists the data:

private class InMemoryUserRepository implements UserRepository
{
    private UserCollection users = new UserCollection;

    public User load(UserIdentifier identifier)
    {
        if (!this.users.exists(identifier)) {
            throw new InvalidUserException;
        }

        return this.users.get(identifier);
    }

    public User find(UserIdentifier identifier)
    {
        if (!this.users.exists(identifier)) {
            return null;
        }

        return this.users.get(identifier);
    }

    public UserCollection fetchAll()
    {
        return this.users;
    }

    public boolean add(User user)
    {
        return this.users.add(user);
    }

    public boolean delete(User user)
    {
        return this.delete(user.getIdentifier());
    }

    public boolean delete(UserIdentifier identifier)
    {
        return this.users.remove(identifier);
    }
}

public class CreateUserServiceTest
{
    @Test
    public void it_should_save_a_new_user()
    {
        UserRepository userRepository = new InMemoryUserRepository;
        CreateUserService sut = new CreateUserService(userRepository);
        sut.createUser(new UserRequestStub);
        assertEquals(new UserCollectionStub, userRepository.fetchAll());
    }
}

I have glossed over the UserRequestStub and UserCollectionStub definitions in this example, their implementations should be fairly simple. The InMemoryUserRepository behaves like any real UserRepository would in your application. You can add and remove Users, search for Users, load Users and all of the return values are correct and valid. The only problem with the InMemoryUserRepository that there is no persistence, so it would be useless in your actual application, but more than adequate as a Collaborator for your tests.

Mocks

Whilst all of the other types of Test Double build on each other, gradually adding more functionality, Mocks are something totally different. Until now, all of our test doubles have made assertions on state. Mocks are test doubles that make assertions on behaviour. Mocks say "I expect you to call foo() with bar, and if you don't there's an error".

Typically, using a Mock will consist of three phases:

  1. Creating the instance
  2. Defining the behaviour
  3. Asserting the calls

Taking our Stub example from above, we could turn that into a Mock like so:

public class FooCollectionTest
{
    @Test
    public void it_should_return_joined_bars()
    {
        Foo fooMock = mock(Foo.class); // instance
        when(fooMock.bar()).thenReturn("baz", "qux"); // behaviour

        FooCollection sut = new FooCollection();
        sut.add(fooMock);
        sut.add(fooMock);

        assertEquals("bazqux", sut.joined());
        verify(fooMock, times(2)).bar(); // verify
    }
}

If we left out the call to verify(), the above example is still a Stub, even though it's using a mocking framework. The difference here is so subtle it's no wonder it leads to confusion. fooMock doesn't become a Mock until we call verify at the end, as that's the key differentiator between a Mock and the other types of Test Doubles.

With the Stub we only assert on the state of the object, but with the Mock we assert that the correct methods were called the correct number of times (and sometimes in the correct order and with the correct parameters).

When To Use Test Doubles

There are two schools of thought on Test Doubles: Classical, and Mockist. Put simply, people who use the Classical style only use Test Doubles when there is a compelling reason to do so, such as external dependencies, whilst a Mockist would mock everything, all the time.

The advantage of using the Classical style is that you don't have to worry about internal behaviour of your objects, which is what OOP is all about. When you change an object and it's collaborators your tests will still pass, but this comes at the sacrifice of speed and isolation.

The advantage of using the Mockist style is that your tests are isolated and can run much faster, but at the sacrifice of coupling your tests to the internal behaviour of your objects and their collaborators.

At this point, no one way is the right way. Like all decisions in software development, it's up to you to use your judgement and select whatever method feels most appropriate for the given situation. Hopefully this post will have given you a solid foundation in the tools available to you, allowing you to make an informed decision next time you find yourself needing a Collaborator in a test.

For exclusive content, including screen-casts, videos, and early beta access to my projects, subscribe to my email list below.


I love discussion, but not blog comments. If you want to comment on what's written above, head over to twitter.