✋ Stop wiring test objects by hand.
✨ Fixtures are now writing themselves.
Annotate a DataSet
and let the compiler produce a fluent *Fixture
API (buildDefault()
, with…
, without…
) so you can express test intent in a couple of lines.
fixture-annotations
— 🏷️ public annotations to mark your DataSet classesfixture-processor
— ⚙️ the annotation processor that generates fixture builders
🔧 Java 21+, Gradle 8+, Maven 3.9+. Works with plain JUnit and Spring Boot.
repositories { mavenCentral() }
// Generate fixtures for application sources (src/main/java)
dependencies {
implementation "io.github.romann-broque:fixture-annotations:x.y.z"
annotationProcessor "io.github.romann-broque:fixture-processor:x.y.z"
}
// Generate fixtures for tests (src/test/java)
dependencies {
testImplementation "io.github.romann-broque:fixture-annotations:x.y.z"
testAnnotationProcessor "io.github.romann-broque:fixture-processor:x.y.z"
}
Assuming you have a Customer
model you want to test:
package org.example.testfixtures.models;
import java.time.LocalDate;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.example.testfixtures.exceptions.CustomerException;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Customer {
private String firstName;
private String lastName;
private String email;
private LocalDate birthDate;
private String phoneNumber;
private String address;
public static Customer create(final String firstName,
final String lastName,
final String email,
final LocalDate birthDate,
final String phoneNumber,
final String address) {
try {
Objects.requireNonNull(firstName, "First name is required");
Objects.requireNonNull(lastName, "Last name is required");
Objects.requireNonNull(email, "Email is required");
Objects.requireNonNull(birthDate, "Birth date is required");
return new Customer(firstName, lastName, email, birthDate, phoneNumber, address);
} catch (final NullPointerException e) {
throw new CustomerException("Failed to create Customer: " + e.getMessage());
}
}
public boolean isAdult() {
return LocalDate.now().isAfter(birthDate.plusYears(18));
}
}
You can create a DataSet
class annotated with @Fixture
.
The annotation processor generates a fluent, chainable builder:
- ⚡
buildDefault()
→ immediately builds the entity using all default values from yourDataModel
. - 🧱
defaultFixture()
→ returns a mutable builder pre-filled with theDataModel
defaults; callbuild()
to create the entity. - 🛠️
with<Field>(value)
→ overrides a single field on the underlyingDataModel
. - 🚫
without<Field>()
→ convenience forwith<Field>(null)
(sets the model field tonull
). - 🔗 All
with…
/without…
methods are chainable; last call wins.
🗂️ Generated sources live under
build/generated/sources/annotationProcessor/java/(main|test)/...
@GenerateFixture(entityClass = Customer.class, dataModelClass = CustomerDataSet.DataModel.class)
public class CustomerDataSet {
public static Customer build(DataModel m) {
return Customer.create(m.firstName, m.lastName, m.email, m.birthDate, m.phoneNumber, m.address);
}
public static class DataModel {
public String firstName = "John";
public String lastName = "Smith";
public String email = "john.smith@corporation.com";
public LocalDate birthDate = LocalDate.of(1990, 1, 1);
public String phoneNumber = "+1234567890";
public String address = "123 Main St, Anytown, USA";
}
}
// Exactly equivalent:
Customer a = CustomerFixture.buildDefault();
Customer b = CustomerFixture.defaultFixture().build();
Customer c = CustomerFixture
.defaultFixture()
.withFirstName("Alice")
.withLastName("Doe")
.withPhoneNumber("+33 6 12 34 56 78")
.build();
Customer d = CustomerFixture
.defaultFixture()
.withoutAddress() // same as .withAddress(null)
.build();
If your factory/constructor enforces non-nulls (e.g., email is required), you can assert failures:
assertThrows(CustomerException.class, () ->
CustomerFixture.defaultFixture().withoutEmail().build()
);
Customer e = CustomerFixture
.defaultFixture()
.withoutPhoneNumber()
.withBirthDate(LocalDate.now().minusYears(25))
.withoutAddress()
.withAddress("42 Rue de la Paix") // last setter wins → address is NOT null
.build();
@ParameterizedTest
@MethodSource("validAdultBirthDateProvider")
void qualifies_as_adult(LocalDate birthDate) {
Customer customer = CustomerFixture.defaultFixture().withBirthDate(birthDate).build();
assertTrue(customer.isAdult());
}
static Stream<Arguments> validAdultBirthDateProvider() {
return Stream.of(
Arguments.of(LocalDate.now().minusYears(18).minusDays(1)),
Arguments.of(LocalDate.now().minusYears(25))
);
}
Once you created the DataSet
class, the fixture will be generated at compile time.
So build your project, and start using the generated *Fixture
class in your tests.
- https://refactoring.guru/design-patterns/builder
- https://ardalis.com/improve-tests-with-the-builder-pattern-for-test-data/
Special thanks to Frédéric Foissey for the original idea and initial implementation of these modules. The current codebase extends and maintains his initial work.
Issues and PRs are welcome. Please include a minimal reproduction for bugs.
- License: Apache-2.0
- Notices: see NOTICE