Understand Test Doubles in less than 5 minutes

Sylvain Tiset
4 min readApr 17, 2024

--

The first time I heard about this word, I didn’t understand it. However, it is something we are used to deal with as a developer. But what exactly are test doubles? What are they used for? Let’s answer these questions.

Test doubles are essential tools in testing in software development. They are objects that simulate the behavior of real components or services, enabling developers to isolate and test individual parts of their code more effectively. Test Double is the generic term for “Mock” and was introduced by Martin Fowler.

Let’s review the different types of test doubles with some quick example.

Test Double

Dummy

A dummy is a placeholder or parameter used in testing to fulfill the requirements of a method or function. Dummy objects are passed around but not used in the test.

They’re typically used when a method requires parameters but the actual values are not needed to the test.

Example

Imagine you have a format method to test in a FormatService. The method has a dependency in parameter. However it’s not used for the final method result.

public final class FormatService {
public final String OUTPUT = "something";

public String format(Dependency dependency) {
// Dependency will not interfere with the expected result.
return OUTPUT;
}
}

In this case you can use a Dummy in the test method (here it will just be “null” value).

import org.junit.Test;
import static org.junit.Assert.assertSame;

public class FormatServiceTest {

@Test
public void testFormat() {
// Notice that the parameter is irrelevant.
String result = new FormatService().format(null);
assertSame(FormatService.OUTPUT, result);
}
}

Stub

Stubs provide pre-defined responses to method calls. In other words, it returns fake data.

They’re useful when you want to control the behavior of a specific method without involving real implementation.

Example

Imagine you want to test a method that return user uuid.

public final class UserService {

public String getUserUuid(UserModelInterface user) {
return user.getUuid();
}
}

You can create a Stub to return a predefined and constant value.

public interface UserModelInterface {
String getUuid();
}

public final class UserStub implements UserModelInterface {
@Override
public String getUuid() {
return "0000-000-000-00001";
}
}

Then you can easily test your method:

import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class UserServiceTest {

@Test
public void testGetUserUuid() {
// The service needs an implementation from UserModelInterface.
String uuid = new UserService().getUserUuid(new UserStub());
assertTrue(uuid.contains("0000-000-000-00001"));
}
}

Spy

A test spy is an object capable of capturing indirect output and providing indirect input as needed. The indirect output is something we cannot directly observe.

They’re useful when you want to verify method calls without altering the behavior of the component being tested.

Example

Imagine you want to spy on your registerUser method to know which user is passed in parameter.

public final class UserNotifier {
private LoggerInterface logger;

public UserNotifier(LoggerInterface logger) {
this.logger = logger;
}

public void registerUser(UserModelInterface user) {
this.logger.log("Notifying the user: " + user.getName());
// ...
}
}

You can use a Spy to do that.

import java.util.ArrayList;
import java.util.List;

public interface LoggerInterface {
void log(String message);
}

public final class LoggerSpy implements LoggerInterface {
public List<String> messages = new ArrayList<>();

@Override
public void log(String message) {
this.messages.add(message);
}
}

Then you can use your LoggerSpy to spy on your registerUser method in a test:

import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class UserNotifierTest {

@Test
public void testLogMessage() {
LoggerSpy logger = new LoggerSpy();
UserNotifier notifier = new UserNotifier(logger);

User user = new User("John");
notifier.registerUser(user);

assertTrue(logger.messages.get(0).contains("Notifying the user: John"));
}
}

Mock

A mock is an object that is capable of controlling both indirect input and output, and it has a mechanism for automatic assertion of expectations and results.

Mocks can throw an exception if they receive an unexpected call and are verified during testing to ensure that all expected calls were made.

Example

Imagine you have a ShoppingService and want to test the following method:

import java.util.List;

public final class ShoppingService {

public float calculateAmount(List<Line> lines) {
float amount = 0;

// Complex code to test, we need a mock for this class
List<Line> linesTransformed = ShoppingCart.transform(lines);

for (Line line : linesTransformed) {
amount += line.price();
}

return amount;
}
}

You can use this Mock to test it:

import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class LoggerTest {

@Test
public void testMovieBudgetFactory() {
ShoppingService service = mock(ShoppingService.class);

List<Line> stubLines = null;

// Overriding the getShoppingCart method with 600f.
when(service.calculateAmount(stubLines))
.thenReturn(Arrays.asList(100, 200, 300));

float totalAmount = service.calculateAmount(stubLines);

// Specify a delta of 0.001 for floating-point comparisons
assertEquals(600f, totalAmount, 0.001);
}
}

This test class requires a few changes in the initial code:

import java.util.List;

// Not final anymore because of the mock
public class ShoppingService {

public float calculateAmount(List<Line> lines) {
float amount = 0;

// Code to test
List<Line> linesTransformed = getShoppingCart(lines);

for (Line line : linesTransformed) {
amount += line.price();
}

return amount;
}

// Protected to have access in the mock object
protected List<Line> getShoppingCart(List<Line> lines) {
return ShoppingCart.transform(lines);
}
}

Fake

A fake is a simpler implementation of real objects.

It can be useful for in-memory databases or mock network services.

Example

You have a getUserById method that return a User.

public class User {
private String uuid;
private String name;
private List<String> roles;

public User(String uuid, String name, List<String> roles) {
this.uuid = uuid;
this.name = name;
this.roles = roles;
}
}

public interface UserRepositoryInterface {
User getUserById(String uuid);
}

You can use a Fake to return a simple and custom result of the method:

public final class FakeUserRepository implements UserRepositoryInterface {
@Override
public User getUserById(String uuid) {
return new User(uuid, "John", Arrays.asList("ADMIN_ROLE"));
}
}

Hoping these definitions and examples made you understand better test doubles.

--

--