Practical Java Test Data Builders with Lombok
This post demonstrates a practical example of a Java Test Data Builder using the Lombok annotations @Setter
and @Accessors(chain = true, fluent = true)
to minimize boilerplate. The concept of using test data builders has been presented in previous blog posts:
The Example Domain Object
The Example
is a simple record that contains a few fields:
aString
is a simple string field closely aligned with how most domain fields will be defined.elements
is a list of owned objects.measurement
is a combination of two fields forvalue
andunit
.stringThatOnlyContainsDigits
demonstrates how you can accept a different type in your test data builder.
The code for Example
is the following:
import java.util.List;
public record Example(
String aString,
List<ExampleListElement> elements,
Measurement measurement,
String stringThatOnlyContainsDigits
) {
public record ExampleListElement(String x, Integer y, Boolean z) {}
public record Measurement(Double value, String unit) {}
}
The Test Data Builder
The test data builder is set up in the following ExampleTestData
:
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
@Setter
@Accessors(chain = true, fluent = true)
public class ExampleTestData {
String aString = "default value";
List<Example.ExampleListElement> elements = new ArrayList<>();
Example.Measurement measurement = new Example.Measurement(1.0, "kg");
String stringThatOnlyContainsDigits = "0123";
public static ExampleTestData builder() {
return new ExampleTestData();
}
public static Example sample() {
return ExampleTestData.builder()
.aString("a string")
.element("x", 1, true)
.measurement(1.0, "kg")
.digit(123)
.build();
}
public ExampleTestData digit(Integer digits) {
this.stringThatOnlyContainsDigits = digits.toString();
return this;
}
public ExampleTestData element(String x, Integer y, Boolean z) {
this.elements.add(new Example.ExampleListElement(x, y, z));
return this;
}
public ExampleTestData measurement(Double value, String unit) {
this.measurement = new Example.Measurement(value, unit);
return this;
}
public Example build() {
return new Example(aString, elements, measurement, stringThatOnlyContainsDigits);
}
}
How the Methods Work
The sample
static method provides a clear example of how you would use the builder. It should only be used when you don't care about the fields of Example
. Otherwise, you use the builder
method in the same way as sample
does in whatever test class needs the data.
The @Setter
and @Accessors(chain = true, fluent = true)
annotations give access to a setter for all fields. For instance, the aString
setter allows chaining the setters. Most fields are set using this pattern.
The digit
method demonstrates how you can set up a method for accepting a type that does not necessarily match the underlying type. While the domain model stores this as a String to preserve leading zeros, most tests don't care about that implementation detail, they just want to specify a number. Using digit(123)
is more intuitive in test code than stringThatOnlyContainsDigits("123")
, making tests more readable and less coupled to internal representation choices.
The element
method shows how you can populate an owned list by providing the exact arguments. This is a key pattern for maintaining clean tests: instead of exposing Example.ExampleListElement
throughout your test suite, the builder provides a simple interface that matches how you think about the data. If the internal structure of ExampleListElement
changes, you only update this one builder method rather than dozens of test files. This decoupling keeps your tests focused on behavior rather than implementation details.
The measurement
method demonstrates another way to create helper methods that prevent exposing internal details and make it easier to use the constructor. Rather than forcing tests to know about the Measurement
record, they can simply call measurement(1.0, "kg")
.
The builder
and build
methods are the standard builder methods that initialize a new builder instance and construct the final object respectively.
Assignments define default values. For example aString
defaults to "default value"
.
Conclusion
This pattern shows how most test data builders can be easily set up in Java with Lombok support. The combination of Lombok's annotation-driven setters for simple fields and custom helper methods for complex scenarios creates builders that are both concise to write and pleasant to use. By hiding implementation details behind builder methods like element()
and digit()
, you create tests that remain stable even as your domain model evolves.