Introduction to Fakes
After we have finished this lesson, we:
- Know what a fake is.
- Understand how fake objects work.
- Understand when we should use fake objects.
This lesson assumes that:
What Is a Fake?
A fake is an object that is used to replace an external dependency of the system under test. Every fake object must fulfill these requirements:
- A fake must provide the same API as the replaced external dependency. This means that if the external dependency is a class, our fake class must extend it and override all methods. On the other hand, if the replaced dependency implements an interface, our fake class must implement the same interface.
- A fake provides a lightweight implementation of the real dependency. It acts like the real thing but it shouldn’t be used in the production environment.
Now we will demonstrate these rules by creating a simple fake. Let’s start by taking a look at the system under test.
Introduction to System Under Test
The system under test has a single dependency called TaskRepository. This interface declares two methods that are used to save Task objects to the used database and query Task objects from it. These methods are described in the following:
- The
findById()method takes the id of the requested task as a method parameter and returns the found task. - The
save()method takes the saved task as a method parameter, saves it to the used database, and returns the information of the saved task.
The source code of the TaskRepository interface looks as follows:
1public interface TaskRepository {
2
3 Task findById(Long id);
4
5 Task save(Task saved);
6}
Let’s find out how we can create a simple fake that can replace TaskRepository objects.
Creating a Simple Fake Object
We can create a fake object 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 fake class as final because it prevents developers from extending it.
The source code of our fake class looks as follows:
1public final class TaskRepositoryFake implements TaskRepository {
2
3}
When I name my fake classes, I like to append the string: ‘Fake’ to the type of the “real” object. Because our fake class is used to replace TaskRepository objects, its name should be: TaskRepositoryFake.
Also, this example assumes that we need to use this fake class from multiple test classes that are not necessarily in the same package as the fake class. However, if this is not the case, we should limit the visibility of our fake class.
Second, we have to create a mechanism that mimics the real database. We can do this by following these steps:
- Add a
private nextIdfield into our fake class. This field contains the id that is assigned to the next task that is saved to our fake database. - Add a
Map<Long, Task>field to into our fake class. ThisMapstores the savedTaskobjects. - Write a constructor that initializes these fields.
The source code of the TaskRepositoryFake class looks as follows:
1import java.util.HashMap;
2import java.util.Map;
3
4public final class TaskRepositoryFake implements TaskRepository {
5
6 private long nextId;
7 private Map<Long, Task> tasks;
8
9 public TaskRepositoryFake() {
10 nextId = 1L;
11 tasks = new HashMap<>();
12 }
13}
Third, we have to implement the findById() method of the TaskRepository interface. We can implement this method by simply fetching the requested Task object from our Map and returning the found object.
After we have implemented this method, the source code of the TaskRepositoryFake class looks as follows:
1import java.util.HashMap;
2import java.util.Map;
3
4public final class TaskRepositoryFake implements TaskRepository {
5
6 private long nextId;
7 private Map<Long, Task> tasks;
8
9 public TaskRepositoryFake() {
10 nextId = 1L;
11 tasks = new HashMap<>();
12 }
13
14 @Override
15 public Task findById(Long id) {
16 return tasks.get(id);
17 }
18}
Fourth, we have to implement the save() method of the TaskRepository interface by following these steps:
- Assign the correct id to the saved
Taskobject. - Put the task to our
Map. - Increase the value of the
nextIdfield by one. - Return the saved
Taskobject.
After we have implemented this method, the source code of the TodoRepositoryFake class looks as follows:
1import java.util.HashMap;
2import java.util.Map;
3
4public final class TaskRepositoryFake implements TaskRepository {
5
6 private long nextId;
7 private Map<Long, Task> tasks;
8
9 public TaskRepositoryFake() {
10 nextId = 1L;
11 tasks = new HashMap<>();
12 }
13
14 @Override
15 public Task findById(Long id) {
16 return tasks.get(id);
17 }
18
19 @Override
20 public Task save(Task saved) {
21 long id = nextId;
22 saved.setId(id);
23
24 tasks.put(id, saved);
25 nextId++;
26
27 return saved;
28 }
29}
We have now created a simple fake object. Next we will write a test method that demonstrates how our fake object works.
Using Our Fake Object
We can create a test class that uses our fake object by following these steps:
First, we have to create a new fake object before our test method is invoked. We can do this by adding a setup method into our test class and creating our fake 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 TaskRepositoryFakeTest {
4
5 private static final String TITLE = "task";
6
7 private TaskRepository repository;
8
9 @Before
10 public void createRepository() {
11 repository = new TaskRepositoryFake();
12 }
13}
Second, we have to write our test method by following these steps:
- Create a new
Taskobject. - Save the created
Taskobject “to the database”. - Verify that the returned
Taskobject contains the correct information. - Get a
Taskobject “from the database” by using the id of the saved task as a search criteria. - Verify that the found
Taskobject contains the correct information.
After we have implemented our test method, the source code of our test class looks as follows:
1import org.junit.Before;
2import org.junit.Test;
3
4import static org.assertj.core.api.Assertions.assertThat;
5
6public class TaskRepositoryFakeTest {
7
8 private static final Long ID = 1L;
9 private static final String TITLE = "task";
10
11 private TaskRepository repository;
12
13 @Before
14 public void createRepository() {
15 repository = new TaskRepositoryFake();
16 }
17
18 @Test
19 public void saveAndFindTask() {
20 Task newTask = new TaskBuilder()
21 .withTitle(TITLE)
22 .build();
23
24 Task saved = repository.save(newTask);
25 assertThat(saved.getId()).isEqualByComparingTo(ID);
26 assertThat(saved.getTitle()).isEqualTo(TITLE);
27
28 Task found = repository.findById(saved.getId());
29 assertThat(found.getId()).isEqualByComparingTo(ID);
30 assertThat(found.getTitle()).isEqualTo(TITLE);
31 }
32}
I think that this example has demonstrated two things:
- Our test method doesn’t know that it is using a fake object because the fake object acts just like the real
TaskRepositoryobject. - No sane person would use this fake object in production code.
It is time to find out why we typically don’t want to create a new fake object before a test method is invoked.
When Should We Use Fake Objects?
When we are deciding whether or not we should use a fake, we must remember that a fake provides a lightweight implementation of the real thing. This means that using a fake has two benefits:
- A fake is easier to configure than the real dependency. Typically fakes require no configuration, and this is crucial when we we are writing integration and end-to-end tests that typically have a somewhat complex configuration.
- A fake is typically a lot faster than the real dependency. Because a fake provides a lightweight implementation of the real thing, we can make tradeoffs that we cannot make in production code.
Even though it is important to understand the benefits of using fakes, it doesn’t really help us to understand when we should use them. That is why we must compare fakes with stubs and mocks.
The main benefits of each test double are:
- A stub helps us to control the response that is returned when its method is invoked.
- A mock helps us to verify the interactions that happen between the system under test and the mock in question.
- A fake acts like the real thing, but it is either easier to use or faster than the real dependency.
When we compare the benefits of using fakes with the benefits of using stubs and mocks, we notice that:
- Stubs are useful when we want to control the behavior of the replaced dependencies.
- Mocks are useful when we want to control the behavior of the replaced dependencies and verify the interactions that happen between the system under test and the mock in question.
- Fakes are useful when we want to replace a problematic dependency with a test double that acts like the real thing. In other words, we don’t want to configure the response that is returned when a method of our test double is invoked and we don’t want to verify the interactions between the system under test and our test double.
That is why I think that we should use fakes when we are writing integration and end-to-end tests, and we want to replace an external dependency that is either too hard to configure or too slow.
The reason why I think this way is that if we are writing integration or end-to-end tests, we only want to know if the whole system works as expected. Fakes make this task easier because we don’t need to configure the expected response. We can simply replace the real dependency with a fake and assume that our test cases are using the real dependency.
For example, we can replace the real database with the H2 in-memory database or we can replace an external REST API with a fake API.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- A fake is an object that replaces an external dependency of the system under test.
- A fake must provide the same API as the replaced external dependency.
- A fake acts like the real thing but it shouldn’t be used in the production environment.
- We should use a fake if configuring the real dependency is too hard or using the real dependency makes our tests too slow.