Skip to content

Commit 6af6a17

Browse files
Add readme
1 parent 2a31287 commit 6af6a17

File tree

8 files changed

+278
-1
lines changed

8 files changed

+278
-1
lines changed

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Frédéric Foissey <ffoissey@users.noreply.github.com>
2+
Romann Broque <romann-broque@users.noreply.github.com>

NOTICE

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fixtures
2+
Copyright (c) 2025 Romann Broque
3+
4+
This product includes code originally authored by:
5+
- Frédéric Foissey (initial initiative and implementation)
6+
7+
Licensed under the Apache License, Version 2.0.
8+
See the LICENSE file distributed with this work for the terms.

README.md

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# fixtures
2+
3+
[![Maven Central](https://img.shields.io/maven-central/v/io.github.romann-broque/fixture-annotations.svg?label=maven%20central)](https://central.sonatype.com/artifact/io.github.romann-broque/fixture-annotations)
4+
[![Build](https://github.com/romann-broque/fixtures/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/romann-broque/fixtures/actions)
5+
[![Javadoc](https://javadoc.io/badge2/io.github.romann-broque/fixture-annotations/javadoc.svg)](https://javadoc.io/doc/io.github.romann-broque/fixture-annotations)
6+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
7+
8+
Stop wiring test objects by hand. Fixtures are now writing themselves. </br>
9+
10+
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.
11+
12+
- **`fixture-annotations`** — public annotations to mark your DataSet classes
13+
- **`fixture-processor`** — the annotation processor that generates fixture builders
14+
15+
> Java 21+, Gradle 8+, Maven 3.9+. Works with plain JUnit and Spring Boot.
16+
17+
---
18+
19+
## Installation
20+
21+
### Gradle (Java)
22+
23+
```groovy
24+
repositories { mavenCentral() }
25+
26+
// Generate fixtures for application sources (src/main/java)
27+
dependencies {
28+
implementation "io.github.romann-broque:fixture-annotations"
29+
annotationProcessor "io.github.romann-broque:fixture-processor"
30+
}
31+
32+
// Generate fixtures for tests (src/test/java)
33+
dependencies {
34+
testImplementation "io.github.romann-broque:fixture-annotations"
35+
testAnnotationProcessor "io.github.romann-broque:fixture-processor"
36+
}
37+
38+
```
39+
40+
### Kotlin (KAPT)
41+
42+
```kotlin
43+
dependencies {
44+
implementation("io.github.romann-broque:fixture-annotations:x.y.z")
45+
kapt("io.github.romann-broque:fixture-processor:x.y.z")
46+
47+
// For tests:
48+
testImplementation("io.github.romann-broque:fixture-annotations:x.y.z")
49+
kaptTest("io.github.romann-broque:fixture-processor:x.y.z")
50+
}
51+
52+
```
53+
54+
### Maven
55+
56+
```xml
57+
<dependencies>
58+
<!-- Generate during main compilation -->
59+
<dependency>
60+
<groupId>io.github.romann-broque</groupId>
61+
<artifactId>fixture-annotations</artifactId>
62+
<version>x.y.z</version>
63+
</dependency>
64+
<dependency>
65+
<groupId>io.github.romann-broque</groupId>
66+
<artifactId>fixture-processor</artifactId>
67+
<version>x.y.z</version>
68+
<scope>provided</scope>
69+
</dependency>
70+
71+
<!-- OR generate during test compilation -->
72+
<!--
73+
<dependency>
74+
<groupId>io.github.romann-broque</groupId>
75+
<artifactId>fixture-annotations</artifactId>
76+
<version>x.y.z</version>
77+
<scope>test</scope>
78+
</dependency>
79+
<dependency>
80+
<groupId>io.github.romann-broque</groupId>
81+
<artifactId>fixture-processor</artifactId>
82+
<version>x.y.z</version>
83+
<scope>test</scope>
84+
</dependency>
85+
-->
86+
</dependencies>
87+
88+
```
89+
90+
---
91+
92+
## Usage example
93+
94+
Assuming you have a `Customer` model you want to test:
95+
96+
```java
97+
package org.example.testfixtures.models;
98+
99+
import java.time.LocalDate;
100+
import java.util.Objects;
101+
import lombok.AccessLevel;
102+
import lombok.AllArgsConstructor;
103+
import lombok.Getter;
104+
import org.example.testfixtures.exceptions.CustomerException;
105+
106+
@Getter
107+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
108+
public class Customer {
109+
private String firstName;
110+
private String lastName;
111+
private String email;
112+
private LocalDate birthDate;
113+
private String phoneNumber;
114+
private String address;
115+
116+
public static Customer create(final String firstName,
117+
final String lastName,
118+
final String email,
119+
final LocalDate birthDate,
120+
final String phoneNumber,
121+
final String address) {
122+
try {
123+
Objects.requireNonNull(firstName, "First name is required");
124+
Objects.requireNonNull(lastName, "Last name is required");
125+
Objects.requireNonNull(email, "Email is required");
126+
Objects.requireNonNull(birthDate, "Birth date is required");
127+
return new Customer(firstName, lastName, email, birthDate, phoneNumber, address);
128+
} catch (final NullPointerException e) {
129+
throw new CustomerException("Failed to create Customer: " + e.getMessage());
130+
}
131+
}
132+
133+
public boolean isAdult() {
134+
return LocalDate.now().isAfter(birthDate.plusYears(18));
135+
}
136+
}
137+
```
138+
139+
### Using the generated Fixture DSL
140+
141+
You can create a `DataSet` class annotated with `@Fixture`.
142+
The annotation processor generates a fluent, chainable builder:
143+
144+
- `buildDefault()` → immediately builds the entity using **all default values** from your `DataModel`.
145+
- `defaultFixture()` → returns a **mutable builder** pre-filled with the `DataModel` defaults; call `build()` to create the entity.
146+
- `with<Field>(value)` → overrides a single field on the underlying `DataModel`.
147+
- `without<Field>()` → convenience for `with<Field>(null)` (sets the model field to `null`).
148+
- All `with…`/`without…` methods are **chainable**; **last call wins**.
149+
150+
> Generated sources live under
151+
> `build/generated/sources/annotationProcessor/java/(main|test)/...`
152+
153+
### Minimal example
154+
155+
```java
156+
@GenerateFixture(entityClass = Customer.class, dataModelClass = CustomerDataSet.DataModel.class)
157+
public class CustomerDataSet {
158+
public static Customer build(DataModel m) {
159+
return Customer.create(m.firstName, m.lastName, m.email, m.birthDate, m.phoneNumber, m.address);
160+
}
161+
public static class DataModel {
162+
public String firstName = "John";
163+
public String lastName = "Smith";
164+
public String email = "john.smith@corporation.com";
165+
public LocalDate birthDate = LocalDate.of(1990, 1, 1);
166+
public String phoneNumber = "+1234567890";
167+
public String address = "123 Main St, Anytown, USA";
168+
}
169+
}
170+
```
171+
#### Build with defaults
172+
173+
```java
174+
// Exactly equivalent:
175+
Customer a = CustomerFixture.buildDefault();
176+
Customer b = CustomerFixture.defaultFixture().build();
177+
```
178+
179+
#### Override selected fields (with…) and chain
180+
181+
```java
182+
Customer c = CustomerFixture
183+
.defaultFixture()
184+
.withFirstName("Alice")
185+
.withLastName("Doe")
186+
.withPhoneNumber("+33 6 12 34 56 78")
187+
.build();
188+
```
189+
190+
#### Explicitly null a field (without…)
191+
192+
```java
193+
Customer d = CustomerFixture
194+
.defaultFixture()
195+
.withoutAddress() // same as .withAddress(null)
196+
.build();
197+
```
198+
If your factory/constructor enforces non-nulls (e.g., email is required), you can assert failures:
199+
```java
200+
assertThrows(CustomerException.class, () ->
201+
CustomerFixture.defaultFixture().withoutEmail().build()
202+
);
203+
```
204+
#### Combine with… and without… freely (order doesn’t matter; last wins)
205+
206+
```java
207+
Customer e = CustomerFixture
208+
.defaultFixture()
209+
.withoutPhoneNumber()
210+
.withBirthDate(LocalDate.now().minusYears(25))
211+
.withoutAddress()
212+
.withAddress("42 Rue de la Paix") // last setter wins → address is NOT null
213+
.build();
214+
```
215+
216+
#### Parameterized tests stay clean and intention-revealing
217+
218+
```java
219+
@ParameterizedTest
220+
@MethodSource("validAdultBirthDateProvider")
221+
void qualifies_as_adult(LocalDate birthDate) {
222+
Customer customer = CustomerFixture.defaultFixture().withBirthDate(birthDate).build();
223+
assertTrue(customer.isAdult());
224+
}
225+
226+
static Stream<Arguments> validAdultBirthDateProvider() {
227+
return Stream.of(
228+
Arguments.of(LocalDate.now().minusYears(18).minusDays(1)),
229+
Arguments.of(LocalDate.now().minusYears(25))
230+
);
231+
}
232+
```
233+
---
234+
235+
## Additional resources
236+
237+
- https://refactoring.guru/design-patterns/builder
238+
- https://ardalis.com/improve-tests-with-the-builder-pattern-for-test-data/
239+
240+
## Thanks
241+
242+
Special thanks to [Frédéric Foissey](https://github.com/ffoissey) for the original idea and initial implementation of these modules. The current codebase extends and maintains his initial work.
243+
244+
## Contributing
245+
246+
Issues and PRs are welcome. Please include a minimal reproduction for bugs.
247+
248+
## Notices & License
249+
250+
- License: [Apache-2.0](./LICENSE)
251+
- Notices: see [NOTICE](./NOTICE)

README.md

Whitespace-only changes.

build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ subprojects {
2626
apply plugin: "maven-publish"
2727
apply plugin: "signing"
2828

29+
tasks.withType(Jar).configureEach {
30+
from("$rootDir/LICENSE") { into "META-INF" }
31+
if (file("$rootDir/NOTICE").exists()) {
32+
from("$rootDir/NOTICE") { into "META-INF" }
33+
}
34+
}
35+
2936
java {
3037
toolchain { languageVersion = JavaLanguageVersion.of(21) }
3138
withSourcesJar()
@@ -47,6 +54,11 @@ subprojects {
4754
}
4855
}
4956
developers {
57+
developer {
58+
id = "ffoissey"
59+
name = "Frédéric Foissey"
60+
email = "ffoissey@users.noreply.github.com"
61+
}
5062
developer {
5163
id = "romann-broque"
5264
name = "Romann Broque"

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
GROUP=io.github.romann-broque
2-
VERSION_NAME=0.1.0
2+
VERSION_NAME=0.1.2

modules/fixture-annotations/src/main/java/io/github/romannbroque/fixture/annotations/package-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* This package contains the core annotations and interfaces for the fixture generation mechanism.
33
*
4+
* <p>Authors: Frédéric Foissey, Romann Broque</p>
5+
*
46
* <p>
57
* The {@link io.github.romannbroque.fixture.annotations.GenerateFixture} annotation is used to mark a class
68
* as a dataset provider for a specific domain entity. This triggers the automatic generation of a

modules/fixture-processor/src/main/java/io/github/romannbroque/fixture/processor/package-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* This package contains annotation processors responsible for generating fixture classes.
33
*
4+
* <p>Authors: Frédéric Foissey, Romann Broque</p>
5+
*
46
* <p>
57
* It leverages the Java Annotation Processing Tool (APT) to automatically generate
68
* builder-like test fixtures based on annotated dataset definitions. The generated fixtures

0 commit comments

Comments
 (0)