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 fake.
After we have finished this blog post, we:
- Know what a fake is.
- Understand how a fake works.
- Understand when we should use fakes.
Let's begin.
What is a Fake?
A fake is a test double that can replace an external dependency of the system under test. A fake must fulfill these requirements:
- A fake must provide the same API as the replaced dependency. This means that if the external dependency is a class, our fake must extend it and override all methods. On the other hand, if the replaced dependency is an interface, our fake must implement the replaced interface.
- A fake provides a lightweight implementation of the real dependency. It acts like the real thing, but typically its implementation takes a shortcut which means that we shouldn't use it in the production environment.
Next, we will put theory into practice and create a new fake. 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 provides CRUD operations for todo items. This interface declares two methods:
- The
findById()
method queries the information of the requested todo item from the database. It takes the id of the requested todo item as a method parameter and returns anOptional
object which contains the found todo item. If no todo item is found from the database, thefindById()
method returns an emptyOptional
object. - The
update()
method updates the information of a todo item that's found from the database. It takes the new information of the updated todo item as a method parameter and returns the information of the updated todo item. If the updated todo item isn't found from the database, this method throws aNotFoundException
.
The source code of the TodoItemRepository
interface looks as follows:
import java.util.Optional; interface TodoItemRepository { Optional<TodoItem> findById(Long id); TodoItem update(TodoItem newInformation); }
The TodoItem
class contains the information of a single todo item. Its source code looks as follows:
class TodoItem { private Long id; private String title; //Getters and setters are omitted }
Let's assume that we have to write unit tests for the update()
method of the TodoItemService
class. This method takes the new information of the updated todo item as a method parameter and returns the information of the updated todo item. The implementation of the update()
method follows these steps:
- Get the updated todo item from the database. If the updated todo item isn't found, throw a
NotFoundException
. - Update the information of the found todo item.
- Return the information of the updated todo item.
The source code of the TodoItemFinderService
class looks as follows:
public class TodoItemService { private final TodoItemRepository repository; public TodoItemService(TodoItemRepository repository) { this.repository = repository; } public TodoItemDTO update(TodoItemDTO newInformation) { var updated = repository.findById(newInformation.getId()) .orElseThrow(() -> new NotFoundException(String.format( "No todo item found with id: #%d", newInformation.getId() ))); updated.setTitle(newInformation.getTitle()); var returned = repository.update(updated); return mapToDTO(returned); } private TodoItemDTO mapToDTO(TodoItem model) { var dto = new TodoItemDTO(); dto.setId(model.getId()); dto.setTitle(model.getTitle()); return dto; } }
The TodoItemDTO
class contains the information of a single todo item. Its source code looks as follows:
public class TodoItemDTO { private Long id; private String title; //Getters and setters are omitted }
The NotFoundException
is an unchecked exception that's thrown if the requested todo item isn't found from the database. The source code of the NotFoundException
class looks as follows:
public class NotFoundException extends RuntimeException { public NotFoundException(String message) { super(message); } }
Let's move on and find out how we can create a fake TodoItemRepository
.
Creating a Simple Fake
When we want to create a fake 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 fake class, its source code looks as follows:
import java.util.Optional; class TodoItemRepositoryFake implements TodoItemRepository { @Override public Optional<TodoItem> findById(Long id) { //Implementation left blank on purpose } @Override public TodoItem update(TodoItem newInformation) { //Implementation left blank on purpose } }
TodoItemRepository
objects, its name should be: TodoItemRepositoryFake
.
Also, this example assumes that the classes which use our fake class are in the same package as the fake class. If this is not the case, we must change the visibility of our fake class.
Second, we have to add one field to the TodoItemRepositoryFake
class. The final todoItems
field contains a Map<Long, TodoItem>
object which acts as an in-memory database. After we have added this field to the TodoItemRepositoryFake
class, its source code looks as follows:
import java.util.HashMap; import java.util.Map; import java.util.Optional; class TodoItemRepositoryFake implements TodoItemRepository { private final Map<Long, TodoItem> todoItems = new HashMap<>(); @Override public Optional<TodoItem> findById(Long id) { //Implementation left blank on purpose } @Override public TodoItem update(TodoItem newInformation) { //Implementation left blank on purpose } }
Third, we have to add a constructor to the TodoItemRepositoryFake
class. This constructor initializes the test data which is used by our unit tests. After we have added this constructor to the TodoItemRepositoryFake
class, its source code looks as follows:
import java.util.HashMap; import java.util.Map; import java.util.Optional; class TodoItemRepositoryFake implements TodoItemRepository { private final Map<Long, TodoItem> todoItems = new HashMap<>(); TodoItemRepositoryFake() { var writeBlogPost = new TodoItem(); writeBlogPost.setId(TodoItems.WriteBlogPost.ID); writeBlogPost.setTitle(TodoItems.WriteBlogPost.TITLE); todoItems.put(TodoItems.WriteBlogPost.ID, writeBlogPost); } @Override public Optional<TodoItem> findById(Long id) { //Implementation left blank on purpose } @Override public TodoItem update(TodoItem newInformation) { //Implementation left blank on purpose } }
The TodoItems
class is a constant class which contains the test data used by our unit tests. Its source code looks as follows:
public final class TodoItems { private TodoItems() {} public static final class WriteBlogPost { public static final Long ID = 1L; public static final String TITLE = "Write a blog post"; } }
Fourth, we have to implement the findById()
method of the TodoItemRepository
interface. This method simply gets the requested todo item from the todoItems
map and returns an Optional
object that contains the found todo item. If the requested todo item isn't found, this method returns an empty Optional
object.
After we have implemented the findById()
method, the source code of the TodoItemRepositoryFake
class looks as follows:
import java.util.HashMap; import java.util.Map; import java.util.Optional; class TodoItemRepositoryFake implements TodoItemRepository { private final Map<Long, TodoItem> todoItems = new HashMap<>(); TodoItemRepositoryFake() { var writeBlogPost = new TodoItem(); writeBlogPost.setId(TodoItems.WriteBlogPost.ID); writeBlogPost.setTitle(TodoItems.WriteBlogPost.TITLE); todoItems.put(TodoItems.WriteBlogPost.ID, writeBlogPost); } @Override public Optional<TodoItem> findById(Long id) { return Optional.ofNullable(todoItems.get(id)); } @Override public TodoItem update(TodoItem newInformation) { //Implementation left blank on purpose } }
Fifth, we have to implement the update()
method of the TodoItemRepository
interface by following these steps:
- Get the updated todo item from the
todoItems
map. If the todo item isn't found, throw aNotFoundException
. - Update the information of the found todo item.
- Return the information of the updated todo item.
After we have implemented the update()
method, the source code of the TodoItemRepositoryFake
class looks as follows:
import java.util.HashMap; import java.util.Map; import java.util.Optional; class TodoItemRepositoryFake implements TodoItemRepository { private final Map<Long, TodoItem> todoItems = new HashMap<>(); TodoItemRepositoryFake() { var writeBlogPost = new TodoItem(); writeBlogPost.setId(TodoItems.WriteBlogPost.ID); writeBlogPost.setTitle(TodoItems.WriteBlogPost.TITLE); todoItems.put(TodoItems.WriteBlogPost.ID, writeBlogPost); } @Override public Optional<TodoItem> findById(Long id) { return Optional.ofNullable(todoItems.get(id)); } @Override public TodoItem update(TodoItem newInformation) { var updated = Optional.ofNullable(todoItems.get(newInformation.getId())) .orElseThrow(() -> new NotFoundException( String.format( "No todo item found with id: #%d", newInformation.getId() ) )); updated.setTitle(newInformation.getTitle()); return updated; } }
We have now written a simple fake. Next, we will write a few unit tests which use our new fake.
Using Our New Fake
Let's assume that we have to write unit tests which ensure that the system under test is working as expected when the updated todo item is found. We can write the required unit tests by following these steps:
First, we have to create a new fake object and replace the TodoItemRepository
dependency of the system under test with the created fake. Because our fake isn't stateless, we have to create a new fake 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:
- Create a new
TodoItemRepositoryFake
object. - Create a new
TodoItemService
object and ensure that the created object uses our fake. - Create a new
TodoItemDTO
object which contains the new information of the updated todo item.
After we have written our setup method, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; class TodoItemServiceTest { private TodoItemRepository repository; private TodoItemService service; private TodoItemDTO input; @BeforeEach void configureSystemUnderTest() { this.repository = new TodoItemRepositoryFake(); this.service = new TodoItemService(this.repository); input = createInput(); } private TodoItemDTO createInput() { var input = new TodoItemDTO(); input.setId(TodoItems.WriteBlogPost.ID); input.setTitle(TodoItems.NEW_TITLE); return input; } }
We also have to add a new constant to the TodoItems
class. This constant contains the new title of the updated todo item. After we have added the required constant to our constant class, the source code of the TodoItems
class looks as follows:
public final class TodoItems { public static final String NEW_TITLE = "Write a new blog post"; private TodoItems() {} public static final class WriteBlogPost { public static final Long ID = 1L; public static final String TITLE = "Write a blog post"; } }
Second, we have to write the test methods which ensure that the system under test is working as expected when the updated todo item is found. We can write these unit tests by following these steps:
- Ensure that the system under test returns the information of the updated todo item.
- Verify that the system under test updates the information of the found todo item.
After we have written these 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.SoftAssertions.assertSoftly; class TodoItemServiceTest { private TodoItemRepository repository; private TodoItemService service; private TodoItemDTO input; @BeforeEach void configureSystemUnderTest() { this.repository = new TodoItemRepositoryFake(); this.service = new TodoItemService(this.repository); input = createInput(); } private TodoItemDTO createInput() { var input = new TodoItemDTO(); input.setId(TodoItems.WriteBlogPost.ID); input.setTitle(TodoItems.NEW_TITLE); return input; } @Test @DisplayName("Should return the information of the updated todo item") void shouldReturnInformationOfUpdatedTodoItem() { var returned = service.update(input); assertSoftly((softAssertions) -> { softAssertions.assertThat(returned.getId()) .as("id") .isEqualByComparingTo(TodoItems.WriteBlogPost.ID); softAssertions.assertThat(returned.getTitle()) .as("title") .isEqualTo(TodoItems.NEW_TITLE); }); } @Test @DisplayName("Should update the information of the found todo item") void shouldUpdateInformationOfFoundTodoItem() { service.update(input); var updated = repository.findById(TodoItems.WriteBlogPost.ID).get(); assertSoftly((softAssertions) -> { softAssertions.assertThat(updated.getId()) .as("id") .isEqualByComparingTo(TodoItems.WriteBlogPost.ID); softAssertions.assertThat(updated.getTitle()) .as("title") .isEqualTo(TodoItems.NEW_TITLE); }); } }
Let's move on and identify the situations when we should use fakes.
When Should We Use Fakes?
Before we can take a closer look at the different dependencies of the system under test, we have to identify the pros and cons of using a fake.
A fake provides these two benefits:
- A fake is easier to configure than the real dependency. Typically fakes require no configuration, and this is crucial when we are writing integration, API, and end-to-end tests that typically have a somewhat complex configuration.
- A fake is typically a lot faster than the real dependency because we can make tradeoffs that we cannot make in production code.
The downside of using a fake is that we must write the code the implements the API of replaced dependency. This means that:
- We can add bugs to our test suite. In other words, if we decide to use a fake, we should be careful and pay extra attention for reviewing the source code of our fake.
- Implementing and reviewing a fake takes time. In other words, it's a lot faster to create a stub or a mock by using a mocking library such as Mockito than to write a fake.
When we think about the dependencies of the system under test, we notice that 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 fakes.
First, if a dependency queries information that's used by the system under test, we should replace it with a fake if we don't want to (or cannot) replace the dependency with a stub. The downside of using a stub is that we have to configure the response that's returned every time when an expected invocation happens between the system under test and our stub, and this can be complicated if we are writing integration, API, or end-to-end tests. If this is the case, it's a good idea to use a fake instead of a stub.
Second, if a dependency triggers an action which has a side effect, we should replace it with a fake if we cannot use a mock. Typically, we cannot use a mock if any of the following conditions is true:
- We care only about the result of the side effect and we don't want to ensure that the expected interactions happen between the system under test and the replaced dependency (the saved item is found from the database vs. an item was saved once by using the correct information).
- It's complicated to ensure that every test method uses a clean mock. Because mocks aren't stateless, we must ensure that the interactions which happened during the execution of the previous test method don't cause problems to the current test method. This might not be trivial if we are writing integration, API, or end-to-end tests.
Third, if a dependency provides utility functions to the system under test, we should use the real dependency because it doesn't make any sense to replace it with a fake. These utility functions require no configuration and they are fast because they have no external dependencies. If we replace this dependency with a fake, we reduce the code coverage of our tests. Also, if we decide to use a fake, we add new code to our test suite and we must ensure that this code is working as expected (this takes time). In other words, I think that we shouldn't replace this dependency with a fake because a fake will only make our test suite less useful.
At this point, we should understand how a fake works and know when we should replace a dependency of the system under test with a fake. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us five things:
- A fake must provide the same API as the replaced dependency.
- A fake acts like the real thing, but typically its implementation takes a shortcut which means that we shouldn't use it in the production environment.
- If a dependency queries information that's used by the system under test, we should replace it with a fake if we don't want to (or cannot) replace the dependency with a stub.
- If a dependency triggers an action which has a side effect, we should replace it with a fake if we care only about the result of the side effect and we don't want to ensure that the expected interactions happen between the system under test and the replaced dependency.
- If a dependency triggers an action which has a side effect, we should replace it with a fake if it's hard to ensure that every test method uses a clean mock.
P.S. You can get the example application of this blog post from Github.