Convenient test data builders in Java
Test data builders are convenient for creating objects used in tests. This is different from using the Builder pattern in production code where it is dangerous and often used to hide away design problems.
The main difference between production builders and test data builders is that for the latter it is very beneficial to have default values. That way I only need to specify these values that are significant for the behavior being tested and can use defaults for everything else.
In languages that support named parameters and default values like Kotlin or Groovy, I don’t need to use the Builder pattern — a factory method with default values will do just fine.
For example, in Kotlin a test data factory for an Order might look like this:
fun anOrder(
lines = listOf(anOrderLine()),
customerId = Id(),
createdAt = Instant.now()
) = Order(
lines = lines,
customerId = customerId,
createdAt = createdAt)
However, in Java, I need to do a bit more work as it doesn’t have either named parameters or default values (and looks like will never have).
Using Instancio
Fortunately, there are libraries that take some boilerplate away from test data builders. Thanks to my colleague Joosep Andrespuk, I learned about a library for generating test data called Instancio. The following is all that is needed to create an Order with some random data using Instancio (static imports skipped):
var order = create(Order.class);
To specify a concrete value or constraints for some fields Insancio has a very nice fluent API:
var order = of(Order.class)
.set(field(Order::lines), ofList(OrderLine.class).size(3).create())
.set(field(Order::createdAt), Instant.now())
.set(field(Order::customerId), new Id(1L))
Note that Order::customerId
is a reference to the “getter” method. If Order
were a class and not a record type then it would be Order::getCustomerId
.
Despite this convenient API, it’s still a bit verbose for my taste to use directly from tests. Therefore, I prefer creating a thin wrapper around Instancio like this:
public class OrderTestBuilder {
private final InstancioApi<Order> builder = of(Order.class);
public static OrderTestBuilder anOrder() {
// set defaults here so that can reuse builder methods
// instead of accessing InstancioApi directly
return new OrderTestBuilder().delivery(Delivery.FAST);
}
public OrderTestBuilder customerId(Id<Customer> customerId) {
builder.set(field(Order::customerId), customerId);
return this;
}
public OrderTestBuilder lines(OrderLine... lines) {
builder.set(field(Order::lines), asList(lines));
return this;
}
public OrderTestBuilder delivery(Delivery delivery) {
builder.set(field(Order::delivery), delivery);
return this;
}
public Order build() {
return builder.create();
}
}
Production Builder wrapper
If for whatever reason I already have Builder in production code then I can take advantage of it and create a test data factory method that sets reasonable defaults.
In the following example, OrderBuilder
is a Builder in production code:
public class TestOrder {
public static OrderBuilder anOrder() {
return Order.builder()
.delivery(Delivery.FAST)
.customerId(new Id(123))
.createdAt(Instant.now());
}
}
With this setup, the test code doesn’t know or care what happens behind the scenes. It can construct an Order
with defaults and some specific values like this:
var order = anOrder().delivery(Delivery.NORMAL).build();