Introduction to Dummies

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 is null.
  • 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.

A good rule of thumb is that if we set a value of a mandatory property and we don't write an assertion for it, we should use a dummy when the set value of the 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 the String: 'NOT IMPORTANT'.
If the system under test has preconditions which are enforced immediately when we invoke it, we must make sure that our insane values don't violate these preconditions. Unfortunately, sometimes these preconditions prevent us from us using insane values. If this happens, we can use a "normal" value and name our constant or local variable in a way which makes it clear that the constant or local variable contains a dummy.

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.
0 comments… add one

Leave a Reply