Introduction to Mocks
After we have finished this lesson, we:
- Know what a mock is.
- Understand how mock objects work.
- Understand when we should use mocks.
This lesson assumes that:
What Is a Mock?
A mock is an object that is used to replace an external dependency of the system under test. Every mock must fulfill these requirements:
- A mock must provide the same API as the replaced external 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 implements an interface, our mock must implement the same interface.
- We must be able to configure the response that is returned when a method of a mock is invoked. This means that we can configure the mock to either return an object or throw an exception.
- A mock returns the configured response every time when its method is invoked.
- We must be able to configure the expected values or objects that should be passed as method parameters when a method of a mock is invoked.
- A mock provides a way to verify the interactions that happen between the system under test and the mock in question.
Now we will demonstrate these rules by creating a simple mock. Let’s start by taking a look at the system under test.
Introduction to System Under Test
The system under test has a single depencency called TaskRepository. This interface declares one method called deleteById() that takes the id of the deleted task as a method parameter and returns the deleted task.
The source code of the TaskRepository interface looks as follows:
1public interface TaskRepository {
2
3 Task deleteById(Long id);
4}
Let’s find out how we can create a simple TaskRepository mock.
Creating a Simple Mock
We can create a mock that can replace the real TaskRepository object by following these steps:
First, we have to create a public and final class that implements the TaskRepository interface. It is a good idea to mark our mock class as final because it prevents developers from extending it.
The source code of our mock class looks as follows:
1public final class TaskRepositoryMock implements TaskRepository {
2
3 public TaskRepositoryMock() {
4 }
5}
When I name my mock classes, I like to append the string: ‘Mock’ to the type of the “real” object. Because our mock class is used to replace TaskRepository objects, its name should be: TaskRepositoryMock.
Also, this example assumes that we need to use this mock class from multiple test classes that are not necessarily in the same package as the mock class. However, if this is not the case, we should limit the visibility of our mock class.
Second, we have to write a method that is used to configure the Task object which is returned when the deleteById() method is invoked. We can implement this method by following these steps:
- Add a
private TaskField into theTaskRepositoryMockclass. This field contains a reference to the returnedTaskobject. - Add a
setReturnedTask()method into theTaskRepositoryMockclass. This methods takes the returnedTaskobject as method parameter and it doesn’t return anything. - Implement the
setReturnedTask()method by setting the returned task to theTaskfield.
After we have implemented this method, the source code of our mock class looks as follows:
1public final class TaskRepositoryMock implements TaskRepository {
2
3 private Task returnedTask;
4
5 public TaskRepositoryMock() {
6 }
7
8 public void setReturnedTask(Task returnedTask) {
9 this.returnedTask = returnedTask;
10 }
11}
Third, we have to write a method that is used to configure the expected id that should be passed to the deleteById() method as a method parameter. We can implement this method by following these steps:
- Add a
private expectedIdArgumentfield into to theTaskRepositoryMockclass. This field contains the id that should be passed to thedeleteById()method as a method parameter. - Add a
setExpectedIdArgument()method into theTaskRepositoryMockclass. This method takes the expected id as a method parameter and it doesn’t return anything. - Implement the
setExpectedIdArgument()method by setting the expected id to theexpectedIdArgumentfield.
After we have implemented this method, the source code of our mock class looks as follows:
1public final class TaskRepositoryMock implements TaskRepository {
2
3 private Long expectedIdArgument;
4 private Task returnedTask;
5
6 public TaskRepositoryMock() {
7 }
8
9 public void setExpectedIdArgument(Long expectedIdArgument) {
10 this.expectedIdArgument = expectedIdArgument;
11 }
12
13 public void setReturnedTask(Task returnedTask) {
14 this.returnedTask = returnedTask;
15 }
16}
Fourth, we have to implement the deleteById() method of the TaskRepository interface by following these steps:
- Add a
private actualIdArgumentfield into our mock class. This field contains the actual id that is passed as a method parameter when thedeleteById()method is invoked. - Add a
private deleteByIdCalledfield into our mock class and set its value tofalse. This field is used to determine if thedeleteById()method was invoked by the system under test. - Implement the
deleteById()method. Remember to set the field values of our new fields and return the configuredTaskobject.
After we have have implemented the deleteById() method, the source code of our mock class looks as follows:
1public final class TaskRepositoryMock implements TaskRepository {
2
3 private Long actualIdArgument;
4 private boolean deleteByIdCalled = false;
5 private Long expectedIdArgument;
6 private Task returnedTask;
7
8 public TaskRepositoryMock() {
9 }
10
11 @Override
12 public Task deleteById(Long id) {
13 actualIdArgument = id;
14 deleteByIdCalled = true;
15
16 return returnedTask;
17 }
18
19 public void setExpectedIdArgument(Long expectedIdArgument) {
20 this.expectedIdArgument = expectedIdArgument;
21 }
22
23 public void setReturnedTask(Task returnedTask) {
24 this.returnedTask = returnedTask;
25 }
26}
Fifth, we have to implement the method which is used to verify that the deleteById() method was invoked and the expected id was passed as a method parameter. We can implement this method by following these steps:
- Add a
verify()method into theTaskRepositoryMockclass. This method takes no method parameters and it doesn’t return anything. - Implement the method by writing the required assertions with AssertJ.
After we have implemented the verify() method, the source code of our mock class looks as follows:
1public final class TaskRepositoryMock implements TaskRepository {
2
3 private Long actualIdArgument;
4 private boolean deleteByIdCalled = false;
5 private Long expectedIdArgument;
6 private Task returnedTask;
7
8 public TaskRepositoryMock() {
9 }
10
11 @Override
12 public Task deleteById(Long id) {
13 actualIdArgument = id;
14 deleteByIdCalled = true;
15
16 return returnedTask;
17 }
18
19 public void setExpectedIdArgument(Long expectedIdArgument) {
20 this.expectedIdArgument = expectedIdArgument;
21 }
22
23 public void setReturnedTask(Task returnedTask) {
24 this.returnedTask = returnedTask;
25 }
26
27 public void verify() {
28 assertThat(deleteByIdCalled)
29 .overridingErrorMessage(
30 "Expected that deleteById() was called but it was not"
31 )
32 .isTrue();
33
34 assertThat(actualIdArgument)
35 .overridingErrorMessage(
36 "Invalid id argument. Expected: %d but was: %d",
37 expectedIdArgument,
38 actualIdArgument
39 )
40 .isEqualByComparingTo(expectedIdArgument);
41 }
42}
We have now created a simple mock. Next we will write two test methods which demonstrate how our mock works.
Using Our Mock Object
If we want to use a mock object in our tests, we must understand that mock objects are not stateless. This means that we have to create a new mock object before a test method is invoked. We can do this by adding a setup method into our test class and creating our mock object in that method.
After we have added this setup method into our test class, its source code looks as follows:
1import org.junit.Before;
2
3public class TaskRepositoryMockTest {
4
5 private static final Long TASK_ID = 44L;
6
7 private TaskRepositoryMock repository;
8
9 @Before
10 public void createMock() {
11 repository = new TaskRepositoryMock();
12 }
13}
We can now write two test methods that demonstrate how our mock object works.
First, we have to write a test method which verifies that the deleteById() method was invoked and the correct id was passed as a method parameter. We can implement this method by following these steps:
- Configure the id that should be passed as a method parameter when the
deleteById()method is invoked. - Invoke the
deleteById()method of theTaskRepositoryinterface. - Verify that the
deleteById()method was invoked and the correct id was passed as a method parameter.
The source code of our test method looks as follows:
1import org.junit.Before;
2import org.junit.Test;
3
4public class TaskRepositoryMockTest {
5
6 private static final Long TASK_ID = 44L;
7
8 private TaskRepositoryMock repository;
9
10 @Before
11 public void createMock() {
12 repository = new TaskRepositoryMock();
13 }
14
15 @Test
16 public void deleteById_ShouldInvokeCorrectMethodWithCorrectId() {
17 repository.setExpectedIdArgument(TASK_ID);
18 repository.deleteById(TASK_ID);
19 repository.verify();
20 }
21}
Second, we have to write a test method which verifies that the deleted Task object is returned when the deleteById() method is invoked. We can write this method by following these steps:
- Create a new
Taskobject. - Configure the
Taskobject that is returned when thedeleteById()method is invoked. - Invoke the
deleteById()method of theTaskRepositoryinterface. - Write an assertion which verifies that the correct
Taskobject was returned.
The source code of our test method looks as follows:
1import org.junit.Before;
2import org.junit.Test;
3
4import static org.assertj.core.api.Assertions.assertThat;
5
6public class TaskRepositoryMockTest {
7
8 private static final Long TASK_ID = 44L;
9
10 private TaskRepositoryMock repository;
11
12 @Before
13 public void createMock() {
14 repository = new TaskRepositoryMock();
15 }
16
17 @Test
18 public void deleteById_ShouldReturnDeletedTask() {
19 Task deleted = new TaskBuilder()
20 .withId(TASK_ID)
21 .build();
22
23 repository.setReturnedTask(deleted);
24
25 Task returned = repository.deleteById(TASK_ID);
26 assertThat(returned).isSameAs(deleted);
27 }
28}
This example has demonstrated that a mock is a test double that:
- Allows us to configure the response that is returned when its method is invoked.
- Returns the same response every time when its method is invoked.
- Allows us to configure the expected values or objects that should be passed as method parameters when a method of a mock is invoked.
- Provides a way to verify the interactions that happen between the system under test and the mock in question.
Also, even though our mock object is a bit naive, the basic principles shown here are valid even if we are using a mocking library.
Next we will find out when we should use mock objects.
When Should We Use Mocks?
Typically a system under test has two kinds of external dependencies:
- Dependencies which query information that is used by the system under test. These dependencies can read information from the used database, fetch it from an external API, and so on.
- Dependencies which trigger an action that has a “side effect”. These dependencies can save information to the used database, send an HTTP request to an external API, trigger an event, and so on.
Because we can verify that an interaction happened between the system under test and a mock, we should use mocks in situations where we want to verify that the system under test actually invoked a method of our mock object.
If we compare this argument with the external dependencies of a system under test, we notice that:
- If the dependency is only a datasource, we don’t have to verify the interactions that happen between the system under test and the external dependency, because we can use assertions for verifying that the system under test is working as expected.
- If the dependency triggers an action that has a “side effect”, we have to verify that the system under test either invoked the correct method of the external dependency or did not invoke it. If we don’t do this, we cannot verify that the system under test is working as expected.
In other words, we should replace an external dependency of the system under test with a mock if the external dependency triggers an action that has a “side effect”.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- A mock is an object that replaces an external dependency of the system under test and returns the configured response every time when its method is invoked.
- A mock allows us to configure the expected values or objects that should be passed as method parameters when a method of a mock is invoked.
- A mock provides a way to verify the interactions that happen between the system under test and the mock in question.
- We should replace an external dependency of the system under test with a mock if the external dependency triggers an action that has a “side effect”.