[JAVA] Object Mother pattern and Test Data Builder pattern

Introduction

Background and purpose

Setting up a test fixture is tedious and difficult. In particular, preparing test data for complex object structures is time-consuming, and redundant descriptions increase the noise of the test code, resulting in poor readability. This article describes the ʻObject Mother pattern and the Test Data Builder pattern`, which are typical design patterns for setting up test fixtures, and also introduces ideas for improvement.

Sample code

Java + JUnit5 sample code is available on GitHub.

What is a test fixture?

A certain state (environment or data) that guarantees the same result when the test is executed repeatedly is called a test fixture. This post will focus on setting up the data needed to run the test.

sample

As a sample for explanation, assume a book management application. Suppose you have the following test code that verifies the check method that checks at the time of lending, with the book lending management service LoanService as the test target (SUT: System Under Test).

SimpleLoanServiceTest.java


public class SimpleLoanServiceTest {

    private static final LocalDate TODAY = LocalDate.now();
    private static final LocalDate YESTERDAY = TODAY.minusDays(1);
    private static final LocalDate A_WEEK_AGO = TODAY.minusDays(7);

    private LoanService sut;

    @BeforeEach
    void setup() {
        sut = new LoanService(TODAY);
    }
...
    @Test
void If the maximum number of loans is exceeded, it will not be possible to lend.() {
        // Arrange
        Book book1 = new Book("Pillow Book");
        Loan loan1 = new Loan(book1, A_WEEK_AGO, TODAY);
        Book book2 = new Book("The Tale of Genji");
        Loan loan2 = new Loan(book2, A_WEEK_AGO, TODAY);

        Loans loans = Loans.of(loan1, loan2);

        User user = new User("Yamamoto", TODAY, loans);

        Book[] books = new Book[]{new Book("The Tale of the Taketori"), new Book("The Tale of the Heike")};

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

    @Test
void If the loaned item has an expiration date, new loan will not be possible.() {
        // Arrange
        Book book1 = new Book("Pillow Book");
        Loan loan1 = new Loan(book1, A_WEEK_AGO, TODAY);
        Book book2 = new Book("The Tale of Genji");
        Loan loan2 = new Loan(book2, A_WEEK_AGO, YESTERDAY);

        Loans loans = Loans.of(loan1, loan2);

        User user = new User("Yamamoto", TODAY, loans);

        Book book = new Book("The Tale of the Taketori");

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));
    }
...

The above test code has the following problems.

The code that sets up test data in the + Arrange clause is redundant and duplicated between test cases.

How can we solve these problems?

Object Mother pattern

If you simply want to eliminate code duplication, you can cut out the test data setup process as a private method in the test class, but similar setup code is written in multiple test classes in duplicate. There is a risk of losing it. ʻObject Mother is a collection of factory methods that set up test data in one place. Below is a ʻObject Mother that provides a factory method to set up a ʻUser`.

UserMother.java


public class UserMother {

    public static User aUserWithoutLoans() {
        Loans loans = Loans.empty();
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing2Books() {
        Loans loans = Loans.of(aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing3Books() {
        Loans loans = Loans.of(aLoan(), aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }
...

The test case looks like this:

ObjectMotherLoanServiceTest.java


    @Test
void If the maximum number of loans is exceeded, it will not be possible to lend.() {
        // Arrange
        User user = UserMother.aUserBorrowing2Books();

        Book[] books = BookMother.books(2);

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

One of the features of ʻObject Mother is that it promotes reuse across test classes and test cases by consolidating factory methods in one place, but the greatest utility is to name the data pattern. I think it's what to do. Proper naming can improve the readability of your test code. For example

User user = UserMother.aUserBorrowing2Books();

Noisy information (in that test case) such as user name, book title, and date is eliminated from the code, and the test condition of "any user who borrows two books" can be read at a glance. I will. On the other hand, ʻObject Mother` also has the following disadvantages.

Test Data Builder pattern

Test Data Builder is a method to set test data using Builder pattern which is one of GoF design patterns. Let's look at an example.

UserBuilder.java


public class UserBuilder {

    private String name = "DEFAULT_NAME";
    private LocalDate expirationDate = LocalDate.MAX;
    private Loans loans = Loans.empty();

    public UserBuilder withLoans(Loans loans) {
        this.loans = loans;
        return this;
    }

    public UserBuilder withExpirationDate(LocalDate expirationDate) {
        this.expirationDate = expirationDate;
        return this;
    }

    public User build() {
        return new User(name, expirationDate, loans);
    }
}

The test code looks like this:

TestDataBuilderLoanServiceTest.java


    @Test
void If the loaned item has an expiration date, new loan will not be possible.() {
        // Arrange
        Loan loan1 = new LoanBuilder().build();
        Loan loan2 = new LoanBuilder().withDueDate(YESTERDAY).build();

        Loans loans = Loans.of(loan1, loan2);

        User user = new UserBuilder().withLoans(loans).build();

        Book book = new BookBuilder().build();

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));

    }

Generally, Test Data Builder defines the default value of each property, and to create an object using the default value, just generate a builder and call the build method as shown below.

new LoanBuilder().build();

If there is a property that you want to change from the default value, after creating the Builder, connect it with a method chain and describe it, and finally call the build method.

User user = new UserBuilder().withLoans(loans).build();

While Test Data Builder improves the visibility of test code by using default values and fluent API using method chains, it also has the following drawbacks:

Test Data Builder improvements

Based on Test Data Builder, I will introduce a technique to improve its shortcomings. First, since object creation with the new operator is noise, change the constructor to private and prepare a static factory method instead.

UserBuilder.java


    private UserBuilder() {}

    public static UserBuilder ofDefault() {
        return new UserBuilder();
    }

In addition to the default values, the factory methods also provide commonly used setup patterns.

UserBuilder.java


    public static UserBuilder borrowing(int numOfBooks) {
        UserBuilder builder = new UserBuilder();
        List<Loan> loanList = new ArrayList<>();
        for (int i = 0; i < numOfBooks; i++) {
            Loan loan = LoanBuilder.ofDefault().build();
            loanList.add(loan);
        }
        builder.loans = Loans.of(loanList.toArray(new Loan[0]));
        return builder;
    }

Writing test code using this factory method has the effect of keeping the code clean and the factory method'naming makes the intent clear. It's like designing a DSL (Domain Specific Language).

HybridLoanServiceTest.java


    @Test
void If the maximum number of loans is exceeded, it will not be possible to lend.() {
        // Arrange
        User user = UserBuilder.borrowing(2).build();

        Book[] books = BookBuilder.ofDefault().build(2);

        // Act
        LoanCheckResult result = sut.check(user, books);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("LIMIT_EXCEEDED"));
    }

There is also a technique for receiving callbacks to give you more flexibility in setting up nested objects. The following sample is an example of receiving a callback to set up a Loan object nested in a ʻUser object using LoanBuilder`.

UserBuilder.java


   public UserBuilder borrowing(Consumer<LoanBuilder> callback) {
        LoanBuilder loanBuilder = LoanBuilder.ofDefault();
        callback.accept(loanBuilder);
        loans.add(loanBuilder.build());
        return this;
    }

Such an implementation pattern is called the Loan pattern because the UserBuilder side creates a LoanBuilder object and lends it to the received callback. * It was a coincidence that the word Loan matched (^ _ ^;) The test code sample to be used is as follows. Callbacks are written in lambda expressions.

HybridLoanServiceTest.java


    @Test
void If the loaned item has an expiration date, new loan will not be possible.() {
        // Arrange
        User user = UserBuilder.ofDefault()
                    .borrowing(loanBuilder -> loanBuilder.noop())
                    .borrowing(loanBuilder -> loanBuilder.withDueDate(YESTERDAY))
                    .build();

        Book book = BookBuilder.ofDefault().build();

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));
    }

Summary

Recommended Posts

Object Mother pattern and Test Data Builder pattern
Test Data Builder pattern ~ Improve maintainability of test code
Builder pattern
Builder Pattern
Design pattern ~ Builder ~
Design pattern (2): Builder
String and stringbuffer and string builder
Builder pattern (Effective Java)
Introduction to Effective java by practicing and learning (Builder pattern)