How to Create a Test Data Builder

Remove duplication and increase expressiveness in test data construction

Testing

In this article, we will learn how to write test data builders that remove duplication in constructing objects and increase the expressiveness of the test code. We will also learn how to use Lombok to reduce boilerplate code around the builders.

In a previous article, we talked about how to remove duplication while at the same time making the code more descriptive. This article is a more practical guide concentrating on the test data builder pattern.

🧰 Constructing Complex Test Data

Let’s imagine we have an Order class with a Customer and a customer has an Address. Also, an Order has one or more OrderItems. Furthermore, the Order might involve a discountRate or some couponCode.

Orders, order items, customers and addresses
Orders, order items, customers and addresses

We might want to test a lot of business logic around these objects:

  1. Check that we cannot apply a discount rate and coupon code at the same time
  2. Check that the quantity of items does not exceed the inventory balance
  3. Check that we apply different shipping rate on a foreign address

For different use cases, we have to create objects in different state. In our production code, we create things in relatively few places. However, in tests, we have to provide all the constructor arguments every time creating an object.

Let’s take a look at a code example.

public class Order {
    private final Long orderId;
    private final Customer customer;
    private final List<OrderItem> orderItems = new ArrayList<>();
    private final Double discountRate;
    private final String couponCode;
    
    public Order(Long orderId, Customer customer, Double discountRate, String couponCode) {
        // ...
    }
    
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
    }

    // ...
}

public class Customer {
    private final Long customerId;
    private final String name;
    private final Address address;
    
    // ...
}

In this example, we use constructors and immutable objects, so they do not have setter methods. The test code becomes quite hard to read.

    @Test
    void constructOrderWithForeignAddress() {
        Address address = new Address("1216  Clinton Street", "Philadelphia", "19108", "United States");
        Customer customer = new Customer(1L, "Terry Tew", address);
        Order order = new Order(1L, customer, 0.0, null);
        order.addOrderItem(new OrderItem("Coffee mug", 1));
        
        // ...
    }

The code is full of details that are irrelevant to the behavior that we test. The result is very noisy. Also, tests become brittle because adding any new parameters will break a lot of tests.

πŸ‘΅ Reduce Duplication With the Object Mother Pattern

The object mother pattern is an attempt to avoid the before-mentioned problems. An object mother is a class with factory methods for different use cases in tests.

Let’s take a look at a couple of object mothers.

public class Orders {
    public static Order createOrderWithCustomer(Customer customer) {
        return new Order(1L, customer, 0.0, null);
    }
}

public class Customers {
    public static Customer createCustomerWithAddress(Address address) {
        return new Customer(1L, "Unimportant", address);
    }
}

public class Addresses {
    public static Address createAddressWithCountry(String country) {
        return new Address("Some street", "Some city", "Some postal code", country);
    }
}

// ...

Now we call our factory methods from the test code.

    @Test
    void constructOrderWithForeignAddress() {
        Address address = Addresses.createAddressWithCountry("United States");
        Customer customer = Customers.createCustomerWithAddress(address);
        Order order = Orders.createOrderWithCustomer(customer);
        order.addOrderItem(OrderItems.createOrderItem("Coffee mug"));
        
        // ...
    }

The object mother pattern makes tests more readable and hides code that creates new objects. We can provide safe values for fields without having to pollute the test code with those values. It also helps with maintenance because we can reuse the code between tests.

However, the object mother pattern is not flexible when test data varies. Every small change in test data requires a new factory method. Having to change the object mother for a lot of different reasons violates the Single Responsibility Principle.

πŸ—οΈ Make Construction Easier With the Builder Pattern

The Builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The Builder design pattern intends to separate the construction of a complex object from its representation.

For classes that require complex setup we can create a test data builder. The builder has a field for each constructor parameter.

public class OrderBuilder {
    private Long orderId;
    private Customer customer;
    private List<OrderItem> orderItems = new ArrayList<>();
    private Double discountRate;
    private String couponCode;

    public OrderBuilder withId(Long orderId) {
        this.orderId = orderId;
        return this;
    }

    public OrderBuilder withCustomer(Customer customer) {
        this.customer = customer;
        return this;
    }

    public OrderBuilder withOrderItem(OrderItem orderItem) {
        this.orderItems.add(orderItem);
        return this;
    }

    public OrderBuilder withDiscountRate(Double discountRate) {
        this.discountRate = discountRate;
        return this;
    }

    public OrderBuilder withCouponCode(String couponCode) {
        this.couponCode = couponCode;
        return this;
    }

    public Order build() {
        Order order = new Order(orderId, customer, discountRate, couponCode);
        orderItems.forEach(order::addOrderItem);
        return order;
    }
}

We provide the actual values using public “with” methods which can be chained.

    @Test
    void constructOrderWithForeignAddress() {
        Order order = new OrderBuilder()
                .withId(1L)
                .withCustomer(new CustomerBuilder()
                        .withCustomerId(1L)
                        .withName("Terry Tew")
                        .withAddress(new AddressBuilder()
                                .withStreet("1216  Clinton Street")
                                .withCity("Philadelphia")
                                .withPostalCode("19108")
                                .withCountry("United States")
                                .build()
                        )
                        .build()
                )
                .withOrderItem(new OrderItemBuilder()
                        .withName("Coffee mug")
                        .withQuantity(1)
                        .build()
                )
                .build();
    }

The example doesn’t provide any advantage over the object mother yet. Next, let’s look at how we can improve the situation.

πŸ™ˆ Set Safe Default Values to Hide Details

If our classes expect some values to exist, our test code has to provide these values. However, a lot of times, these values might not be relevant to the test case. We want to hide the details that are irrelevant to the test.

Let’s change the way we build the dependent objects.

public class OrderBuilder {

    private Long orderId = 1L;
    private Customer customer = new CustomerBuilder().build();
    private List<OrderItem> orderItems = new ArrayList<>();
    private Double discountRate = 0.0;
    private String couponCode;
  
    // ...
}

public class AddressBuilder {
    private String street = "Some street";
    private String city = "Some city";
    private String postalCode = "Some postal code";
    private String country = "Some country";

    // ...
}

By setting a default value to the customerBuilder field and any other fields, we provide safe values for these fields. This way, we can omit any fields that are not relevant to our test but require a value.

    @Test
    void constructOrderWithForeignAddress() {
        Order order = new OrderBuilder()
                .withCustomer(new CustomerBuilder()
                        .withAddress(new AddressBuilder().withCountry("United States").build())
                        .build()
                )
                .withOrderItem(new OrderItemBuilder()
                        .withName("Coffee mug")
                        .withQuantity(1)
                        .build()
                )
                .build();
    }

Now the test data builder can hide details like the object mother pattern does. However, the builder is more flexible than the object mother.

➑️ Simplify Code by Passing Builders as Arguments

In our builder example, the builder consumes some arguments that are objects built by other builders. If we pass those builders as arguments instead of the constructed objects, we can simplify the code by removing the calls to build() methods.

public class OrderBuilder {
    // ...

    public OrderBuilder withCustomer(CustomerBuilder customerBuilder) {
        this.customer = customerBuilder.build();
        return this;
    }
    
    // ...
}

Now using the builder becomes less verbose.

    @Test
    void constructOrderWithForeignAddress() {
        Order order = new OrderBuilder()
            .witCustomer(new CustomerBuilder()
                    .withAddress(new AddressBuilder().withCountry("United States"))
            )
            .withOrderItem(new OrderItemBuilder().withName("Coffee mug").withQuantity(1))
            .build();

We were able to get rid of some syntax noise.

🏭 Emphasize Domain With Factory Methods

There is still some noise in the tests because we have to construct various builders. We can reduce this noise by adding factory methods for the builders.

public class OrderBuilder {
    // ...

    private OrderBuilder() {}

    public static OrderBuilder anOrder() {
        return new OrderBuilder();
    }

    // ...
}

We are still duplicating the name of the constructed type in both the “with” methods and the builder names. We can take advantage of the type system and shorten the names of the “with” methods.

public class OrderBuilder {
    // ...

    public OrderBuilder with(CustomerBuilder customerBuilder) {
        this.customer = customerBuilder.build();
        return this;
    }

    // ...
}

By using static imports we can avoid mentioning the word “builder” in the tests.

    @Test
    void buildOrderWithForeignAddress() {
        Order order = anOrder()
                .with(aCustomer().with(anAddress().withCountry("United States")))
                .with(anOrderItem().withName("Coffee mug").withQuantity(1))
                .build();
    }

The factory methods hide a lot of details about the builders. The code is now more descriptive as it speaks in domain terms.

β™Š Reduce Code When Creating Similar Objects

If we need to create similar objects, we could use different builders for them. However, different builders lead to duplication and make it harder to spot differences.

Let’s take a look at the following example.

    Order orderWithSmallDiscount = anOrder()
            .with(anOrderItem().withName("Coffee mug").withQuantity(1))
            .with(anOrderItem().withName("Tea cup").withQuantity(1))
            .withDiscountRate(0.1)
            .build();
    Order orderWithBigDiscount = anOrder()
            .with(anOrderItem().withName("Coffee mug").withQuantity(1))
            .with(anOrderItem().withName("Tea cup").withQuantity(1))
            .withDiscountRate(0.5)
            .build();

Because of the repetition in the construction, the difference with the discount rate gets hidden in the noise. We can extract a builder with a joint state and then provide the differing values for each object separately.

    OrderBuilder coffeeMugAndTeaCup = anOrder()
            .with(anOrderItem().withName("Coffee mug").withQuantity(1))
            .with(anOrderItem().withName("Tea cup").withQuantity(1));

    Order orderWithSmallDiscount = coffeeMugAndTeaCup.withDiscountRate(0.1).build();
    Order orderWithBigDiscount = coffeeMugAndTeaCup.withDiscountRate(0.5).build();

By reusing the common parts and naming the variables descriptively, the differences become much more apparent.

There is one pitfall in this approach, though. Let’s take a look at another example.

    OrderBuilder coffeeMugAndTeaCup = anOrder()
            .with(anOrderItem().withName("Coffee mug").withQuantity(1))
            .with(anOrderItem().withName("Tea cup").withQuantity(1));

    Order orderWithDiscount = coffeeMugAndTeaCup.withDiscountRate(0.1).build();
    Order orderWithCouponCode = coffeeMugAndTeaCup.withCouponCode("HALFOFF").build();

We would expect that only the first order has the discount rate applied. However, since calling the builder methods affects the builder’s state, the second order will have a discount rate applied as well!

One way to solve this is to add a method that returns a copy of the builder.

public class OrderBuilder {
    // ...
    
    private OrderBuilder(OrderBuilder copy) {
        this.orderId = copy.orderId;
        this.customerBuilder = copy.customerBuilder;
        this.orderItems = copy.orderItems;
        this.discountRate = copy.discountRate;
        this.couponCode = copy.couponCode;
    }

    public OrderBuilder but() {
        return new OrderBuilder(this);
    }

    // ...
}

Now we can make sure that the changes from previous uses do not leak to the next one.

    OrderBuilder coffeeMugAndTeaCup = anOrder()
            .with(anOrderItem().withName("Coffee mug").withQuantity(1))
            .with(anOrderItem().withName("Tea cup").withQuantity(1));
  
    Order orderWithDiscount = coffeeMugAndTeaCup.but().withDiscountRate(0.1).build();
    Order orderWithCouponCode = coffeeMugAndTeaCup.but().withCouponCode("HALFOFF").build();

There is still room for human error, so if we want to be safe, we could make the “with” methods always return a copy of the builder.

🌢️ Reduce Boilerplate With Lombok

While the test data builder pattern provides a lot of benefits, there is also one major drawback. That is, we end up writing a lot of boilerplate code.

To tackle this problem with boilerplate, we can take advantage of the Lombok project. We can get rid of the default constructor, getters and automatically create a builder class by annotating the class with Lombok @Data and @Builder annotations.

@Data
@Builder
public class Order {
    private final Long orderId;
    private final Customer customer;
    private final List<OrderItem> orderItems;
    private final Double discountRate;
    private final String couponCode;
}

Lombok automatically creates a builder class and a factory method inside the class.

    OrderItem coffeeMug = OrderItem.builder().name("Coffee mug").quantity(1).build();
    OrderItem teaCup = OrderItem.builder().name("Tea cup").quantity(1).build();
    Order order = Order.builder()
            .orderItems(Arrays.asList(coffeeMug, teaCup))
            .build();

Unfortunately, for collections, you now have to pass a collection as the builder argument. Luckily, by annotating a collection field with @Singular, Lombok will generate methods for single items in the collection.

@Data
@Builder
public class Order {
    private final Long orderId;
    private final Customer customer;
    @Singular
    private final List<OrderItem> orderItems;
    private final Double discountRate;
    private final String couponCode;
}

We can now add items to a collection by calling the method multiple times.

    Order order = Order.builder()
            .orderItem(OrderItem.builder().name("Coffee mug").quantity(1).build())
            .orderItem(OrderItem.builder().name("Tea cup").quantity(1).build())
            .build();

We have still lost a little in the readability, at least in my opinion. We have to carry the word “builder” around.

However, we can configure Lombok to generate a different name for the factory method and use static imports. We can also prefix the setter methods.

@Data
@Builder(builderMethodName = "anOrder", setterPrefix = "with")
public class Order {
    // ...
}

Now our builder looks a lot more like the custom builder.

    Order order = anOrder()
            .withOrderItem(anOrderItem().withName("Coffee mug").withQuantity(1).build())
            .withOrderItem(anOrderItem().withName("Tea cup").withQuantity(1).build())
            .build();

There is still one problem, though. This code now suffers from the same problem as our custom builder when we try to vary our data. In our custom builder, we added a but() method to deal with this.

Luckily, Lombok allows us to configure a toBuilder() method.

@Data
@Builder(builderMethodName = "anOrder", toBuilder = true, setterPrefix = "with")
public class Order {
    // ...
}

The difference here is that the toBuilder() is called on a constructed object, not on a builder.

    Order coffeeMugAndTeaCup = anOrder()
            .withOrderItem(anOrderItem().withName("Coffee mug").withQuantity(1).build())
            .withOrderItem(anOrderItem().withName("Tea cup").withQuantity(1).build())
            .build();

    Order orderWithDiscount = coffeeMugAndTeaCup.toBuilder().withDiscountRate(0.1).build();
    Order orderWithCouponCode = coffeeMugAndTeaCup.toBuilder().withCouponCode("HALFOFF").build();

Now Lombok builder is pretty close to the expressiveness of our custom builder.

The obvious benefit of using Lombok is that we get rid of a lot of boilerplate code. However, our code is also a little noisier because we have to write, e.g.:

    .withOrderItem(anOrderItem().withName("Coffee mug").withQuantity(1).build())

Instead of:

    .with(anOrderItem().withName("Coffee mug").withQuantity(1))

There is one more issue, though. There are no safe default values for our fields. We could add default values in our production code, but it’s not a good idea to do that only for tests.

πŸ’ž Combine Builders and Object Mothers

To deal with the problem of not having safe default values, we can take the idea of the object mother pattern and use that together with our Lombok-generated builders.

public class Orders {
    public static Order.OrderBuilder anOrder() {
        return Order.anOrder()
                .withOrderId(1L)
                .withCustomer(Customers.aCustomer().build())
                .withDiscountRate(0.0);
    }
}

public class Customers {
    public static Customer.CustomerBuilder aCustomer() {
        return Customer.aCustomer()
                .withCustomerId(1L)
                .withName("Unimportant")
                .withAddress(Addresses.anAddress().build());
    }
}

Instead of using the Lombok-generated factory method, we can use the factory method from the object mother. The only thing we have to do is to change the static imports. Basically we are calling Orders.anOrder() instead of Order.anOrder(), for example.

Since we cannot pass around builders as arguments, the code is still a little noisier than our custom builder. Another drawback is that our builder will now always unnecessarily construct defaults for objects whose value we override in our tests.

βœ… Summary

Test data builders offer several benefits over constructing objects by hand. They help to hide syntax noise related to creating objects. They make the simple cases simple, and special cases are not much more complicated. Test data builders also make the code easier to read and are resilient to changes in the object structure.

We can also take advantage of the Lombok builders combined with the object mother pattern. While we lose a bit in the compactness, we reduce a lot of the boilerplate code.

You can find the example code for this article on Codeberg.

Additional reading:

πŸ“– Growing Object-Oriented Software, Guided by Tests by Steve Freeman, Nat Pryce

best-practices

Archived comments

4 reactions

πŸ‘ 4

Comments

youkaba

Thanks for sharing great article :) I discover your blog.
So I’ve question about lombok. If I consider Order and OrderItem like entities. we have bidirectional oneToMany. In that case we need to add some additional methods like addItems to link Order and OrderItem. it looks like this

public void addItems(OrderItem item) {
        orderItems.add(actor);
        item.setItem(this);
    }

if you use @Builder, lombok generate something like this on method build :
orderItems = Collections.unmodifiableList(new ArrayList(this.orderItems)); we noticed we don`t have item.setItem(this);. So if you save Order, in OrderItem table we see order is not linked.

So in that case how we can combine a Builder and Object mother to replace those additional methods ?

Thanks again
your blog is just wealth of information :)

πŸ‘ 2
arhohuttunen

Thanks for the kind words!

As far as I know there is no way to handle the bidirectional relationship via Lombok builders. Of course, while not ideal, you can always add those methods manually.

Just a word of warning though, make sure Lombok doesn’t generate equals() or hashCode() methods for your JPA entities as it will cause problems. Also toString() could be problematic.

AymaneHrouch
Very detailed article!
Thank you!!
❀️ 1
CamilYed

Wow nice article!

https://github.com/projectlombok/lombok/issues/3398 - I hope that maybe they will improve lombok.

πŸ‘ 1
CamilYed

It looks that Lombok support default values:

    /**
	 *  The field annotated with {@code @Default} must have an initializing expression; that expression is 
     *  taken as the default to be used if not explicitly set during building.
	 */
	 @Target(FIELD)
	 @Retention(SOURCE)
	 public @interface Default {}
arhohuttunen
They do, but the problem is that it will become a default value for the production code. If that is ok with you, you could use that, but I’ve found more often than not that you need different defaults for your test data builders or object mothers.
CamilYed
Wait wait…
Test Data Builder is the class under test source set…
arhohuttunen

Maybe there is some misunderstanding from my part, but the Lombok @Builder annotation is added to the actual entity we want to create. I’m not aware of a way to put that annotation on a separate class that would then create that entity. Thus, it’s not in the test source set.

In my example, only the hand-made builders are under test sources.

CamilYed

Have you tried sth like below. Let’s assume that the Order Class is under test source set.

@AllArgsConstructor
@NoArgsConstructor
public class Order {
    @Default @With private Long orderId = 1L;
    private Customer customer;
    private List<OrderItem> orderItems = Lists.of(anOrderItem().build())
    @Default @With private Double discountRate = 10d;
    @Default @With private String couponCode = "xaxadxa";

    Order withOrderItems(OrderItem.OrderItemBuilder... items) {
        this.items =  Arrays.stream(items).map(OrderItem.OrderItemBuilder::build).toList();
        return this;
    }
}   
arhohuttunen
I’m not sure how that helps because it would just make Order insccessible in production code. And if you make a duplicate in the test sources, then the builder is not creating an object of the same type anymore.
CamilYed

Then you need to add a build method and copy field by field to create an instance of your domain object.

BTW first of all, I would avoid using a production class as a builder. This approach has some drawbacks.

arhohuttunen
It works ok when you would use the builder pattern in your production code as well, but it does have drawbacks like also mentioned in the article. I personally go with the custom classes and don’t mind the boilerplate.
vadim-vainshtein
Arho, thank you for your great articles. You are the best!
❀️ 1
arhohuttunen
Thanks for the compliments!
albloptor
arhohuttunen
This article has been written in February 2021 with no major changes since. From what I can tell your article has been published one and a half year after that.
albloptor
Nice article! Great job explaining this topic in depth! πŸ‘