Introduction to Mocks

When we are writing automated tests for our code, often we notice that it's not possible to invoke the real dependencies of the system under test. The problem might be that:

  • The problematic dependency invokes an external API which cannot be accessed from our test environment.
  • We cannot invoke the real dependency because invoking it would cause unwanted side effects.
  • The real dependency is too slow and invoking it would slow down our test suite.

If we cannot use the real dependency, we have to replace it with a test double that must provide the same API as the replaced dependency. This ensures that the system under test will think that it's interacting with the real thing.

There are multiple different test doubles and each test double helps us to solve a very specific problem. This time we will take a closer look at a test double called a mock.

After we have finished this blog post, we:

  • Know what a mock is.
  • Understand how a mock works.
  • Understand when we should use mocks.

Let's begin.

By the way, Feedspot.com featured my blog on their article: 15 Best Spring Framework Blogs and Websites. I recommend that you take a look at that article because you might find something new to read (I know I did).

This blog post assumes that:

What Is a Mock?

A mock is a test double which helps us to ensure that the expected interactions happen between the system under test and a mock. A mock must fulfill these requirements:

  • A mock must provide the same API as the replaced dependency. This means that if the external dependency is a class, our mock must extend it and override all methods. On the other hand, if the replaced dependency is an interface, our mock must implement the replaced interface.
  • We must be able to configure the response that's returned every time when an expected interaction happens between the system under test and a mock. This means that we can configure the mock to either return an object or throw an exception.
  • When an unexpected invocation happens between the system under test and a mock, a mock can either return a default response (such as null, an empty Optional, or an empty collection) or throw an exception.
  • A mock must allow us to verify the interactions that happen between the system under test and a mock. In other words, we must be able to identify the invoked method, configure the number of expected method invocations, and specify the expected argument(s) which are passed to the invoked method.
Next, I will clarify the terms expected and unexpected invocation:

  • An expected invocation means that the system under test invokes the stubbed method and passes the expected arguments to the invoked method.
  • An unexpected invocation means that the system test either invokes a method that's not stubbed or invokes a stubbed method and passes unexpected arguments to the invoked method.

Additional Reading:

Next, we will put theory into practice and create a new mock. Let's start by taking a quick look at the system under test.

Introduction to the System Under Test

The system under test has one dependency (TodoItemRepository) which declares a method that deletes the specified todo item from the database. This method (deleteById()) takes the id of the deleted todo item as a method parameter and returns a TodoItem object which contains the information of the deleted todo item. If no todo item is found from the database, the deleteById() method throws a NotFoundException.

The source code of the TodoItemRepository interface looks as follows:

interface TodoItemRepository {

    TodoItem deleteById(Long id);
}

The TodoItem class contains the information of a single todo item. Its source code looks as follows:

public class TodoItem {
 
    private Long id;
    private String title;
 
    //Getters an setters are omitted
}

Let's assume that we have to write unit tests for the deleteById() method of the TodoItemService class. This method simply invokes the deleteById() method of the TodoItemRepository interface and returns the information of the deleted todo item.

The source code of the TodoItemService class looks as follows:

public class TodoItemService {

    private final TodoItemRepository repository;

    public TodoItemService(TodoItemRepository repository) {
        this.repository = repository;
    }

    public TodoItem deleteById(Long id) {
        return repository.deleteById(id);
    }
}

Let's move on and find out how we can create a simple TodoItemRepository mock.

Creating a Simple Mock

When we want to create a mock that can replace the real TodoItemRepository dependency, we have to follow these steps:

First, we have to create a new class and ensure that this class implements the TodoItemRepository interface. After we have created our mock class, its source code looks as follows:

class TodoItemRepositoryMock implements TodoItemRepository {

    @Override
    public TodoItem deleteById(Long id) {
        //Implementation left blank on purpose
    }
}
When I name my mock classes, I like to append the string: 'Mock' to the type of the "real" object. Because our mock class can replace TodoItemRepository objects, its name should be: TodoItemRepositoryMock.

Also, this example assumes that the classes which use our mock class are in the same package as the mock class. If this is not the case, we must change the visibility of our mock class.

Second, we have to add five fields to the TodoItemRepositoryMock class. These fields are:

  • The final expectedIdArgument field contains the id of the deleted todo item which must be passed to the deleteById() method as an argument.
  • The final returned field contains the information of the deleted todo item that's returned by the deleteById() method.
  • The final todoItemNotFound field contains a boolean value which determines if the deleted todo item is found from the database. If this value is true, the deleteById() method throws a NotFoundException.
  • The actualIdArgument field contains the id that was passed to the deleteById() method as an argument when it was invoked by the system under test.
  • The deleteByIdInvocationCount field contains the number of method invocations. The default value of this field is zero.

After we have added these fields to the TodoItemRepositoryMock class, its source code looks as follows:

class TodoItemRepositoryMock implements TodoItemRepository {

    private final Long expectedIdArgument;
    private final TodoItem returned;
    private final boolean todoItemNotFound;

    private Long actualIdArgument;
    private int deleteByIdInvocationCount = 0;

    @Override
    public TodoItem deleteById(Long id) {
        //Implementation left blank on purpose
    }
}

Third, we have to add three constructors to the TodoItemRepositoryMock class. We can write these constructors by following these steps:

  1. Add a private default constructor to the TodoItemRepositoryMock class. This constructor disables the no-argument constructor and ensures that we can compile the TodoItemRepositoryMock class.
  2. Write a constructor which takes the expected id argument as a constructor argument. This constructor ensures that the deleteById() method throws a NotFoundException when the system under test invokes the deleteById() method and passes the expected id to invoked method as an argument.
  3. Write a constructor which takes the information of the deleted todo item as a constructor argument. This constructor ensures that the deleteById() method returns the information of the deleted todo item when it's invoked by the systen under test.

After we have added these constructors to the TodoItemRepositoryMock class, its source code looks as follows:

class TodoItemRepositoryMock implements TodoItemRepository {

    private final Long expectedIdArgument;
    private final TodoItem returned;
    private final boolean todoItemNotFound;

    private Long actualIdArgument;
    private int deleteByIdInvocationCount = 0;
    
    private TodoItemRepositoryMock() {
        //These lines ensure that this class can be compiled.
        this.expectedIdArgument = null;
        this.returned = null;
        this.todoItemNotFound = false;
    }

    TodoItemRepositoryMock(Long expectedIdArgument) {
        this.expectedIdArgument = expectedIdArgument;
        this.returned = null;
        this.todoItemNotFound = true;
    }

    TodoItemRepositoryMock(TodoItem returned) {
        if (returned == null) {
            throw new NullPointerException("The returned todo item cannot be null");
        }

        if (returned.getId() == null) {
            throw new IllegalArgumentException("The id of the todo item cannot be null");
        }

        this.expectedIdArgument = returned.getId();
        this.returned = returned;
        this.todoItemNotFound = false;
    }

    @Override
    public TodoItem deleteById(Long id) {
        //Implementation left blank on purpose
    }
}

Fourth, we have to implement the deleteById() method by following these steps:

  1. Increase the value of the deleteByIdInvocationCount field by one and store the id argument in the actualIdArgument field.
  2. If the method invocation is unexpected, throw a new UnexpectedInteractionException. A method invocation is unexpected if the id argument is null or it's not equal to the value of the expectedIdArgument field.
  3. If the method invocation is expected, follow these rules:
    • If the value of the todoItemNotFound field is true, throw a NotFoundException.
    • If the value of the todoItemNotFound field is false, return the information of the deleted todo item.

After we have implemented the deleteById() method, the source code of the TodoItemRepositoryMock class looks as follows:

class TodoItemRepositoryMock implements TodoItemRepository {

    private final Long expectedIdArgument;
    private final TodoItem returned;
    private final boolean todoItemNotFound;

    private Long actualIdArgument;
    private int deleteByIdInvocationCount = 0;

    private TodoItemRepositoryMock() {
        //These lines ensure that this class can be compiled.
        this.expectedIdArgument = null;
        this.returned = null;
        this.todoItemNotFound = false;
    }

    TodoItemRepositoryMock(Long expectedIdArgument) {
        this.expectedIdArgument = expectedIdArgument;
        this.returned = null;
        this.todoItemNotFound = true;
    }

    TodoItemRepositoryMock(TodoItem returned) {
        if (returned == null) {
            throw new NullPointerException("The returned todo item cannot be null");
        }

        if (returned.getId() == null) {
            throw new IllegalArgumentException("The id of the todo item cannot be null");
        }

        this.expectedIdArgument = returned.getId();
        this.returned = returned;
        this.todoItemNotFound = false;
    }

    @Override
    public TodoItem deleteById(Long id) {
        this.deleteByIdInvocationCount++;
        this.actualIdArgument = id;

        if (invocationIsExpected(id)) {
            if (todoItemNotFound) {
                throw new NotFoundException(String.format(
                        "No todo item found with the id: %d",
                        id
                ));
            }

            return returned;
        }

        throw new UnexpectedInteractionException(
                "Unexpected method invocation. Expected that id is: %d but was: %d",
                this.returned.getId(),
                id
        );
    }

    private boolean invocationIsExpected(Long id) {
        return (id != null) && id.equals(this.expectedIdArgument);
    }
}
I think that a stubbed method must always throw an exception when it notices an unexpected method invocation and provide an error message which explains why the exception was thrown. This approach has two benefits:

  • The test which invokes the stubbed method will fail.
  • We know immediately why the test failed.

Fifth, we have to add a public verify() method to our mock class. This method doesn't take any method parameters and it doesn't return anything. After we have added this method to our mock class, we must implement it by following these steps:

  1. Verify that the deleteById() method was invoked exactly one time.
  2. Ensure that the expected id was passed to the deleteById() method as an argument when it was invoked by the system under test.

After we have implemented the verify() method, the source code of the TodoItemRepositoryMock class looks as follows:

import static org.assertj.core.api.Assertions.assertThat;

class TodoItemRepositoryMock implements TodoItemRepository {

    private final Long expectedIdArgument;
    private final TodoItem returned;
    private final boolean todoItemNotFound;

    private Long actualIdArgument;
    private int deleteByIdInvocationCount = 0;

    private TodoItemRepositoryMock() {
        this.expectedIdArgument = null;
        this.returned = null;
        this.todoItemNotFound = false;
    }

    TodoItemRepositoryMock(Long expectedIdArgument) {
        this.expectedIdArgument = expectedIdArgument;
        this.returned = null;
        this.todoItemNotFound = true;
    }

    TodoItemRepositoryMock(TodoItem returned) {
        if (returned == null) {
            throw new NullPointerException("The returned todo item cannot be null");
        }

        if (returned.getId() == null) {
            throw new IllegalArgumentException("The id of the todo item cannot be null");
        }

        this.expectedIdArgument = returned.getId();
        this.returned = returned;
        this.todoItemNotFound = false;
    }

    @Override
    public TodoItem deleteById(Long id) {
        this.deleteByIdInvocationCount++;
        this.actualIdArgument = id;

        if (invocationIsExpected(id)) {
            if (todoItemNotFound) {
                throw new NotFoundException(String.format(
                        "No todo item found with the id: %d",
                        id
                ));
            }

            return returned;
        }

        throw new UnexpectedInteractionException(
                "Unexpected method invocation. Expected that id is: %d but was: %d",
                this.returned.getId(),
                id
        );
    }

    private boolean invocationIsExpected(Long id) {
        return (id != null) && id.equals(this.expectedIdArgument);
    }

    public void verify() {
        assertThat(deleteByIdInvocationCount)
                .overridingErrorMessage(
                        "Expected one invocation but found %d invocations.",
                        deleteByIdInvocationCount
                )
                .isOne();

        assertThat(actualIdArgument)
                .overridingErrorMessage(
                        "Expected an argument: %d but got an argument: %d",
                        expectedIdArgument,
                        actualIdArgument
                )
                .isEqualTo(expectedIdArgument);
    }
}
To be honest, we don't have to check that the expected id was passed to the deleteById() method as an argument because the deleteById() method throws an exception if the system under test invokes it by using an incorrect aka unexpected id.

However, I added this this assertion here because we have to check the value of the actual id argument if:

  • A stubbed method implemented by our mock class returns the default value if an unexpected invocation occurs.
  • We don't have to stub the invoked method because it doesn't return anything.

We have now written a simple mock. Next, we will write a few test methods which use our new mock.

Using Our New Mock

We can use our new mock by following these steps:

First, we have to create a new mock object and replace the TodoItemRepository dependency of the system under test with the created mock. Because mocks aren't stateless, we have to create a new mock before a test method is invoked. In other words, we have to add a new setup method to our test class and annotate this method with the @BeforeEach annotation. After we have added a new setup method to our test class, we have to implement it by following these steps:

  1. Create the TodoItem object which contains the information of the deleted todo item.
  2. Create a new TodoItemRepositoryMock object and configure the TodoItem object that's returned when the deleteById() method of the TodoItemRepository interface is invoked.
  3. Create a new TodoItemService object and ensure that the created object uses our mock.

After we have written our setup method, the source code of our test class looks as follows:

import org.junit.jupiter.api.BeforeEach;

public class TodoItemServiceTest {

    private static final long TODO_ITEM_ID = 4L;
    private static final String TODO_ITEM_TITLE = "FooBar";

    private TodoItemService service;
    private TodoItemRepositoryMock repository;

    @BeforeEach
    void configureSystemUnderTest() {
        TodoItem deleted = createDeletedTodoItem();
        repository = new TodoItemRepositoryMock(deleted);
        service = new TodoItemService(repository);
    }
}

Second, we can now write tests methods which use our mock by following these steps:

  1. Ensure that the system under test returns a todo item which has the correct id.
  2. Verify that the system under test returns a todo item which has the correct title.
  3. Ensure that the system under test deletes the correct todo item.

After we have written the required test methods, the source code of our test class looks as follows:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class TodoItemServiceTest {

    private static final long TODO_ITEM_ID = 4L;
    private static final String TODO_ITEM_TITLE = "FooBar";

    private TodoItemService service;
    private TodoItemRepositoryMock repository;

    @BeforeEach
    void configureSystemUnderTest() {
        TodoItem deleted = createDeletedTodoItem();
        repository = new TodoItemRepositoryMock(deleted);
        service = new TodoItemService(repository);
    }

    private TodoItem createDeletedTodoItem() {
        TodoItem deleted = new TodoItem();
        deleted.setId(TODO_ITEM_ID);
        deleted.setTitle(TODO_ITEM_TITLE);
        return deleted;
    }

    @Test
    @DisplayName("Should return a todo item with the correct id")
    void shouldReturnTodoItemWithCorrectId() {
        TodoItem deleted = service.deleteById(TODO_ITEM_ID);
        assertThat(deleted.getId()).isEqualTo(TODO_ITEM_ID);
    }

    @Test
    @DisplayName("Should return a todo item with the correct title")
    void shouldReturnTodoItemWithCorrectTitle() {
        TodoItem deleted = service.deleteById(TODO_ITEM_ID);
        assertThat(deleted.getTitle()).isEqualTo(TODO_ITEM_TITLE);
    }

    @Test
    @DisplayName("Should delete the correct todo item")
    void shouldDeleteCorrectTodoItem() {
        service.deleteById(TODO_ITEM_ID);
        repository.verify();
    }
}

Let's move on and identify the situations when we should use mocks.

When Should We Use Mocks?

A system under test can have dependencies that:

  • Query information which is used by the system under test. These dependencies can read information from the database, fetch it from an external API, and so on.
  • Trigger an action which has a side effect. These dependencies can save information to the database, send an HTTP request to an external API, trigger an event, and so on.
  • Provide utility functions to the system under test. These functions are usually stateless and don't use any external services such as databases or APIs. For example, these functions can transform objects into other objects, enforce business or other validation rules, parse information from an object given as an argument, and so on.

Next, we will go through these dependencies one by one and identify the dependencies which should be replaced with mocks.

First, if a dependency queries information that's used by the system under test, we shouldn't replace it with a mock because there is no need to verify the interactions which happen between the system under test and this dependency. Instead, we should replace this dependency with a stub.

If we want to verify that the system under test invokes our stub, we can either write assertions for the object returned by the system under test, write assertions for the exception thrown by the system under test, or ensure that the system under test uses the information returned by our stub when it interacts with other test doubles.

Second, if a dependency triggers an action which has a side effect, we must replace this dependency with a mock because we want to verify that the action is triggered by using the expected information.

Third, if a dependency provides utility functions to the system under test, we should use the real dependency because there is no need to verify the interactions which happen between the system under test and this dependency.

If we want to verify that the system test invokes these utility functions, we can either write assertions for the object returned by the system under test, write assertions for the exception thrown by the system under test, or ensure that the system under test uses the information returned by these functions when it interacts with other test doubles.

At this point, we should understand how a mock works and know when we should replace a dependency of the system under test with a mock. Let's summarize what we learned from this blog post.

Summary

This blog post has taught us five things:

  • A mock must provide the same API as the replaced dependency.
  • We must be able to configure the response that's returned every time when an expected interaction happens between the system under test and a mock.
  • When an unexpected invocation happens between the system under test and a stubbed method of a mock, the stubbed method must throw an exception and provide an error message which explains why the exception was thrown.
  • A mock must allow us to verify the interactions that happen between the system under test and a mock.
  • We must replace a dependency with a mock if the dependency triggers an action which has a side effect.

P.S. You can get the example application this blog post from Github.

2 comments… add one
  • Chris Oct 7, 2022 @ 16:37
    • Petri Oct 8, 2022 @ 0:54

      No. I wasn't writing about fakes. A fake is a test double that that acts like the real thing but it has downsides which prevent you from using it in the production environment. For example, a repository which uses a Map object as a data storage is a fake. Also, a fake provides no way to stub methods or verify the interactions that happen between the system under test and a fake.

Leave a Reply