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 dummy.
After we have finished this blog post, we:
- Know what a dummy is.
- Understand when we should use dummies.
- Know how we should create dummies.
Let's begin.
What Is a Dummy?
A dummy is simply an object or a value that's irrelevant for our test case, but we still have to provide it because:
- Our code won't compile.
- Our test throws an exception because a mandatory value is missing.
Next, we will learn to identify the situations when we should use dummies.
When Should We Use Dummies?
Let's take a look at two examples which demonstrate when we should use dummies.
First, when we write test code that creates a new object by using a constructor or invokes a method, we might notice that all arguments aren't important for our test case. However, we cannot leave these arguments out because our code wouldn't compile. Instead, we should replace these arguments with dummies.
Let’s assume that we are writing unit tests for a method that creates a copy of an existing todo item. This method takes two method parameters:
- The id of the copied todo item.
- A
LoggedInUser
object that contains the information of the user who is marked as the creator of the new todo item.
Also, this method can throw these two exceptions:
- The method throws a
NullPointerException
if any of its method parameters isnull
. - The method throws a
NotFoundException
if the copied todo item isn't found.
The source code of the system under test looks as follows:
import com.google.common.base.Preconditions; import java.util.Optional; public class TodoItemService { private final TodoItemRepository repository; public TodoItemService(TodoItemRepository repository) { this.repository = repository; } public TodoItem createCopyOf(Long sourceId, LoggedInUser creator) { Preconditions.checkNotNull(sourceId); Preconditions.checkNotNull(creator); var found = repository.findById(sourceId); var source = found.orElseThrow(NotFoundException::new); //The rest of this method is omitted } }
Now, if we want to write a unit test which ensures that the system under test throws a NotFoundException
when the copied todo item isn't found, we don’t really care about the second argument (creator
) because it's irrelevant for our test case. However, we still have to pass something as the second argument or our code won't compile. Also, we cannot use null
because our test would fail for the wrong reason. That's why we must use a dummy.
Second, when we create the test data that's required by our test case, sometimes we have to create new objects that have mandatory properties. If these properties aren't specified or an invalid property value is used, our code throws an exception.
For example, the TodoItem
class has a builder which ensures that we cannot create a new TodoItem
object that doesn’t have a creator
and a title
. The source code of the Builder
inner class looks as follows:
public static class Builder { private Long id; private Assignee assignee; private Creator creator; private String description; private String title; private Builder() {} //Other methods are omitted for the sake of clarity public TodoItem build () { var todoItem = new TodoItem(this); checkCreator(todoItem); checkTitle(todoItem); return todoItem; } private void checkCreator(TodoItem todoItem) { var creator = todoItem.getCreator(); if (creator == null) { throw new NullPointerException(); } } private void checkTitle(TodoItem todoItem) { var title = todoItem.getTitle(); if (title == null) { throw new NullPointerException(); } if (title.isEmpty()) { throw new IllegalStateException(); } } }
If we have to write a unit test which verifies that our builder throws a NullPointerException
when we try to create a new TodoItem
object that has no title
, we don't care about the value of the creator
property because it's irrelevant for our test case. However, we must provide it or our builder throws a NullPointerException
and our test passes (and the checkTitle()
method isn't run). Because we want that our unit test runs the checkTitle()
method, we have to use a dummy when we set the value of the creator
property.
We should now understand when we should use dummies. Let's move on and find out how we can create our dummies.
How Should We Create Our Dummies?
There are different techniques which we can use when we have to create a dummy. However, we must always follow these two rules:
- Store the dummy either in a constant or a local variable. This is important because we don't want to add magic numbers to our test code.
- Use a name which makes it clear that the constant or local variable contains a dummy.
Creating dummies isn't rocket science. However, the different options have slight differences, and that's why we will take a look at four different techniques which we can use when we have to create a dummy. These techniques are:
First, if we want to replace a primitive type, a wrapper class, or a String
with a dummy, we can use an insane value. When we use this technique, we should use a value which makes it clear that the value is irrelevant for our test case. For example:
- If the replaced value is normally a positive integer, we could use a negative integer.
- If we want to replace a
String
with a dummy, we could use theString
: 'NOT IMPORTANT'.
Second, we can use the new
keyword. Because a dummy is basically just a "placeholder" that's required when we want to make sure that our code compiles and that our test won't throw an exception because a mandatory value is missing, we don’t have to set the field values of the created object. That's why using the new
keyword is a good choice as long as the class in question provides a no-argument constructor and the visibility of the constructor doesn’t prevent us from using it.
For example, if we have to write a unit test which ensures that the createCopyOf()
method of the TodoItemService
class throws a NotFoundException
when the copied todo item isn't found, we should replace the LoggedInUser
object with a dummy. The following example demonstrates how we can create our dummy by using the new
keyword:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class TodoItemServiceTest { private static final Long SOURCE_TODO_ITEM_ID = 3L; private TodoItemService service; //Setup is omitted because of clarity @Nested @DisplayName("Create a copy of the specified todo item") class CreateCopyOf { @Nested @DisplayName("When the copied todo item isn't found") class WhenCopiedTodoItemIsNotFound { //Setup is omitted because of clarity @Test @DisplayName("Should throw an exception") void shouldThrowException() { var notImportant = new LoggedInUser(); assertThatThrownBy( () -> service.createCopyOf(SOURCE_TODO_ITEM_ID, notImportant) ).isExactlyInstanceOf(NotFoundException.class); } } } }
Third, we can use a mocking framework. This is a good choice if:
- We cannot use the
new
keyword because the replaced class doesn't have a suitable no-argument constructor. - We have to write a new "dummy class" that either implements an interface or extends the replaced class before we can instantiate our dummy by using the
new
keyword.
For example, if we have to write a unit test which ensures that the createCopyOf()
method of the TodoItemService
class throws a NotFoundException
when the copied todo item isn't found, we should replace the LoggedInUser
object with a dummy. The following example demonstrates how we can create our dummy by using Mockito:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; class TodoItemServiceTest { private static final Long SOURCE_TODO_ITEM_ID = 3L; private TodoItemService service; //Setup is omitted because of clarity @Nested @DisplayName("Create a copy of the specified todo item") class CreateCopyOf { @Nested @DisplayName("When the copied todo item isn't found") class WhenCopiedTodoItemIsNotFound { //Setup is omitted because of clarity @Test @DisplayName("Should throw an exception") void shouldThrowException() { var notImportant = mock(LoggedInUser.class); assertThatThrownBy( () -> service.createCopyOf(SOURCE_TODO_ITEM_ID, notImportant) ).isExactlyInstanceOf(NotFoundException.class); } } } }
Fourth, if we cannot use the other techniques, we should create our dummy by using a factory method. The goal of this technique is to move the irrelevant object creation logic from our test methods to the factory method.
If we decide to use this technique, we shouldn’t use our existing factory methods because typically the names of these methods are misleading. The problem is that our existing factory methods aren't designed for creating dummies. That's why the names of these factory methods don't emphasize that the created object is irrelevant for our test case.
Instead, we should create a new factory method, give it a name which states that the created object is a dummy, and create our dummy objects by using that method. Also, if possible, our new factory method should set only the mandatory property values by using insane values (aka dummies).
For example, if we have to write a unit test which ensures that the createCopyOf()
method of the TodoItemService
class throws a NotFoundException
when the copied todo item isn't found, we should replace the LoggedInUser
object with a dummy. The following example demonstrates how we can create our dummy by using a factory method:
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class TodoItemServiceTest { private static final Long SOURCE_TODO_ITEM_ID = 3L; private TodoItemService service; //Setup is omitted because of clarity @Nested @DisplayName("Create a copy of the specified todo item") class CreateCopyOf { @Nested @DisplayName("When the copied todo item isn't found") class WhenCopiedTodoItemIsNotFound { //Setup is omitted because of clarity @Test @DisplayName("Should throw an exception") void shouldThrowException() { var notImportant = LoggedInUserTestFactory.dummyUser(); assertThatThrownBy( () -> service.createCopyOf(SOURCE_TODO_ITEM_ID, notImportant) ).isExactlyInstanceOf(NotFoundException.class); } } } }
At this point, we should know what a dummy is, and understand how we can create and use dummies in our test methods. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us four things:
- A dummy is an object or a value that's irrelevant for our test case.
- We have to provide a dummy to the system under test because otherwise our code won’t compile or our test throws an exception.
- If our dummy is a primitive type, a wrapper class, or a
String
, we should use an insane value (if possible). - If our dummy is a "complex" class, we can create it by using the
new
keyword, a mocking framework, or a factory method.