Skip to main content
pragmatic clean code minimizing cognitive load in production java

Readable Test Data

3 min read Chapter 17 of 25
Summary

Explains how Test Data Builders and Object Mothers...

Explains how Test Data Builders and Object Mothers create readable test data. Builders offer more flexibility and robustness than Object Mothers, improving the signal-to-noise ratio in tests by allowing granular control and hiding irrelevant defaults. Provides a comparison table and a complete Java code example.

Readable Test Data

When crafting tests, the setup of test data often accounts for over 50% of the test code volume. This not only contributes to high maintenance costs but also obscures the intent of the test, making it harder for developers to understand what is being tested. Two patterns emerge as solutions to this problem: the Object Mother and the Test Data Builder. While both aim to simplify test data creation, they differ significantly in their approach and applicability.

Object Mother Pattern

The Object Mother pattern involves creating a class that contains static factory methods for returning pre-configured, valid instances of domain objects. This approach centralizes object creation and can make tests more readable by providing a clear, single source of truth for test data. However, the Object Mother can lead to the ‘Fragile Test’ syndrome if many tests rely on the same global constants, as changing these constants can break numerous unrelated tests.

Test Data Builder Pattern

The Test Data Builder pattern, on the other hand, uses a fluent API to construct complex test objects. This allows the caller to specify only the relevant fields while defaulting others, thus improving the Signal-to-Noise ratio by hiding default values that do not impact the specific test case. The builder pattern solves the problem posed by the Object Mother by allowing for incremental, non-breaking modifications to test data. By using builders, tests become more focused on what is being tested rather than how the data is constructed.

Example Implementation

public class UserBuilder {
    private String id = "UUID-123";
    private String role = "GUEST";
    private boolean isActive = true;

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

    public UserBuilder withAdminRole() {
        this.role = "ADMIN";
        return this;
    }

    public UserBuilder inactive() {
        this.isActive = false;
        return this;
    }

    public UserBuilder withId(String id) {
        this.id = id;
        return this;
    }

    public User build() {
        return new User(id, role, isActive);
    }
}

Usage in Tests

@Test
void should_allow_admin_access() {
    // Signal: We only care that the user is an ADMIN
    User admin = UserBuilder.aUser().withAdminRole().build();
    
    assertTrue(service.canAccessDashboard(admin));
}

Comparison of Patterns

FeatureObject MotherTest Data Builder
FlexibilityLow (Fixed states)High (Granular control)
BoilerplateLowMedium (Requires builder class)
ReadabilityHigh for standard casesHigh for specific variations
MaintenanceBrittle if reused heavilyRobust to constructor changes
Primary UseGeneric, valid entitiesSpecific, edge-case entities

In conclusion, while both the Object Mother and Test Data Builder patterns can improve the readability and maintainability of test data, the Test Data Builder offers more flexibility and robustness, especially in scenarios where test data needs to be customized for specific test cases. By leveraging the Test Data Builder pattern, developers can ensure that their tests remain focused, readable, and easy to maintain.

Sources