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 for value and unit.
  • 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.