DRY test code with the builder and factory pattern

Your test code can give the reader a very good insight into the behaviour of your application and your intent. That’s why I think your test code should be as readable as your production code. But sometimes it’s challenging to keep your test code DRY(don’t repeat yourself), because of the data you need to setup. In this article I want to show you not only how to avoid duplication, but also how to write your test code more descriptive with the help of the builder and factory pattern. The examples are in Java but the concepts should be language agnostic.

Our example application

Let’s build a small Todo List. With 2Test cases

  • “should return all tasks”
  • “should find task by title”

At the beginning we have 3 classes

The Task entity:

public class Task {
private String title;
private String content;
  public Task(String title, String content) {
this.title = title;
this.content = content;
}
  // getters .. //
}

a Task Service

public class TaskService {
private TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
}

And a Test for the TaskService

public class TaskServiceTest {
private TaskService taskService;
private TaskRepository taskRepositoryStub;
  @Before
public setup() {
taskRepositoryStub = new TaskRepositoryStub();
taskService = new TaskService(taskRepositoryStub);
}
}

Now let’s write our first test case

@Test
public void shouldReturnAllTickets() {
// given
List<Task> tasks = asList(new Task("title1", "content1"),
new Task("title2", "content2"));
given(ticketRepository.all()).willReturn(tasks);
  // when
List<Task> actual = taskService.all();
  // then
assertThat(actual).containsExactly(tasks);
}

We already see that the “given” part is a bit noise. We don’t really care, what title or content the Tasks have, but since these are required fields, we have to set them.

Test Builder to the rescue

Let’s create a new TaskTestBuilder class

public class TaskTestBuilder {
private String title;
private String content;
  public TaskTestBuilder(int seed) {
title = "title." + seed;
content = "content." + seed;
}
  public Task build() {
return new Task(title, content);
}
}

We have 2 methods for now: A constructor and a build method. The constructor initialises the default values. Always choose the default values in a way, that they reflect the “happy path”. Imagine the task entity would have a “deleted” timestamp field. If you would set this field in the constructor, chance you have to set this in every test to null are very high. We also provide a seed. With the help of the seed every builded entity will look different, which can be very helpful. The build method will give you back an Instance of a Task, based on the provided values. The test case would now look like this:

@Test
public void shouldReturnAllTickets() {
// given
List<Task> tasks = asList(new TaskTestBuilder(0).build(),
new TaskTestBuilder(1).build());
given(ticketRepository.all()).willReturn(tasks);
  // when
List<Task> actual = taskService.all();
  // then
assertThat(actual).containsExactly(tasks);
}

This looks better now. But still not optimal I think. Let’s introduce a Factory for the builder

public class TaskTestBuilder {
private static int seed = 0;
private String title;
private String content;
  public static TaskTestBuilder aTask() {
return new TaskTestBuilder(seed++);
}
  private TaskTestBuilder(int seed) {
title = "title." + seed;
content = "content." + seed;
}
  public Task build() {
return new Task(title, content);
}
}

The static method will create a new TaskBuilder instance and will also take care of the seed. I always like to start the name of the factory method with an article, because I think that it reads more fluent in the test cases.

Now the Test code looks like this:

@Test
public void shouldReturnAllTickets() {
// given
List<Task> tasks = asList(aTask().build(), aTask().build());
given(ticketRepository.all()).willReturn(tasks);
  // when
List<Task> actual = taskService.all();
  // then
assertThat(actual).containsExactly(tasks);
}

Let’s move on to the next test case.

“should find a task by title”. For this Test case we care about the title of the test instances. Therefore we add a setter method the test builder:

public class TaskTestBuilder {
private static int seed = 0;
private String title;
private String content;
  public static TaskTestBuilder aTask() {
return new TaskTestBuilder(seed++);
}
  private TaskTestBuilder(int seed) {
title = "title." + seed;
content = "content." + seed;
}
  public TaskTestBuilder withTitle(String title) {
this.title = title;
return this;
}
  public Task build() {
return new Task(title, content);
}
}

Now we can just set the title and still use the other default values:

@Test
public shouldFindTaskByTitle() {
// given
String title = "findMe";
Task expected = aTask().withTitle(title).build();
List<Task> tasks = asList(expected, aTask().build())
given(ticketRepository.all()).willReturn(tasks);
  // when
List<Task> actual = taskService.findByTitle(title);
  // then
assertThat(actual).containsExactly(expected);
}

We can now just set the title explicit. Never rely on the default values of a test builder. Always set the needed values for your test explicitly. This makes your test more readable, because you tell the reader what values are important for your test. Also everyone can just change the default values of the test builder without breaking any tests.