Introduction to Fakes

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 an Optional object which contains the found todo item. If no todo item is found from the database, the findById() method returns an empty Optional 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 a NotFoundException.

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:

  1. Get the updated todo item from the database. If the updated todo item isn't found, throw a NotFoundException.
  2. Update the information of the found todo item.
  3. 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
    }
}
When I name my fake classes, I like to append the string: 'Fake' to the type of the "real" object. Because our fake class can replace 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:

  1. Get the updated todo item from the todoItems map. If the todo item isn't found, throw a NotFoundException.
  2. Update the information of the found todo item.
  3. 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:

  1. Create a new TodoItemRepositoryFake object.
  2. Create a new TodoItemService object and ensure that the created object uses our fake.
  3. 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:

  1. Ensure that the system under test returns the information of the updated todo item.
  2. 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.

0 comments… add one

Leave a Reply