This project is a working example of using JFixtures to prepare relational-database data for integration tests. JFixtures reads YAML fixtures, resolves identifiers and relations, and produces SQL that can be executed before a test.
The project uses JFixtures 3.1.0. Its executable examples live in src/test/resources/yaml, and their checks are in src/test/groovy/com/github/rodionovsasha/commentservice.
JFixtures gives every fixture row a readable label, such as homer or stupid_flanders. It derives a deterministic primary-key value for that label unless an id is supplied explicitly. Other fixtures can then refer to the row by label instead of hard-coding database IDs.
The usual workflow is:
- Describe rows in one or more YAML files.
- Optionally describe relationships, generated columns, cleanup, and other rules in a YAML configuration file.
- Load and compile the fixture set with JFixtures.
- Render SQL with
toSql99()or apply the compiled fixtures to a JDBC connection orDataSource.
This keeps fixture data readable and makes foreign keys stable when fixtures are reorganized.
JFixtures is already configured for tests in this project's pom.xml:
<dependency>
<groupId>io.github.rodionovsasha</groupId>
<artifactId>jfixtures</artifactId>
<version>${jfixtures.version}</version>
<scope>test</scope>
</dependency>Keep the dependency in test scope when fixtures are used only to set up tests. This project defines ${jfixtures.version} as 3.1.0.
Put fixtures under src/test/resources. A directory is convenient for a related group of tables. In this repository, src/test/resources/yaml/user contains user.yml, topic.yml, and comment.yml.
Each YAML file represents a table; the file name without .yml is the table name. The top-level keys are row labels and their values are column maps:
# src/test/resources/yaml/user/user.yml
homer:
name: Homer
age: 39
bart:
name: Bart
age: 10
active: falseThe example creates rows in the user table. homer and bart are fixture labels, not values inserted into a column. JFixtures generates the primary keys for them. Application tests can obtain the generated integer ID through the same label:
import io.github.rodionovsasha.jfixtures.IntId
def homerId = IntId.one("homer")That is how BaseTest.groovy refers to the users, topics, and comments prepared by this fixture set. Use a label that is unique within the table and keep it stable so test assertions stay readable.
To use an explicit primary key instead, include the key column in the row. JFixtures validates explicitly repeated primary keys and reports both conflicting labels:
users:
homer:
id: 1
name: Homer
bart:
id: 2
name: BartThe .conf.yml file alongside a fixture directory configures how label values become foreign keys. In this project, the configuration maps topic.owner_id to a user label, and both comment foreign keys to their target tables:
# src/test/resources/yaml/user/.conf.yml
refs:
topic:
owner_id: user
comment:
user_id: user
topic_id: topicWith that configuration, a topic can refer to homer and a comment can refer to both a user and a topic:
# topic.yml
stupid_flanders:
title: Stupid Flanders
owner_id: homer
# comment.yml
comment1:
content: Why you little...!
user_id: homer
topic_id: stupid_flandersJFixtures replaces homer with the generated primary key of the user row and stupid_flanders with the generated primary key of the topic row. It also orders the generated statements so referenced rows are inserted first.
For a self-reference, map the column to the same table name. The ordered-map example in src/test/resources/yaml/features/new-features/ordered uses this configuration:
refs:
categories:
parent_id: categoriesIf parent rows must be emitted before child rows, preserve their order with YAML's !omap:
!omap
- grandparent: { name: Grandparent }
- parent: { name: Parent, parent_id: grandparent }Ordinary YAML values become SQL literals. Prefix an expression with sql: when it must be emitted as SQL instead. For example, the following creates a timestamp at execution time:
stupid_flanders_comment:
content: Eat My Shorts!
date: sql:CURRENT_TIMESTAMP() + 4
user_id: bart
topic_id: stupid_flandersThe project also uses a reusable column concern to add a creation date to every topic and comment row that does not set date itself:
columns:
concerns:
add_creation_date:
date: sql:CURRENT_TIMESTAMP()
apply:
tables_with_date:
to: topic, comment
concerns: add_creation_dateAn explicit row value takes precedence, so a row can override the shared rule as shown in the preceding example.
The following Java setup is equivalent to the Spring test configuration in this project. It loads all .yml files under the fixture directory, applies .conf.yml, generates SQL-99, and runs it through Spring's JdbcTemplate:
import io.github.rodionovsasha.jfixtures.JFixtures;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.jdbc.core.JdbcTemplate;
Path fixturesPath = Paths.get("src/test/resources/yaml/user");
String sql = JFixtures
.withConfig(fixturesPath.resolve(".conf.yml"))
.load(fixturesPath)
.compile()
.toSql99()
.toString();
jdbcTemplate.execute(sql);For a fixture set that does not need configuration, start with noConfig():
String sql = JFixtures.noConfig()
.load(Paths.get("src/test/resources/yaml/features/values"))
.compile()
.toSql99()
.toString();You can skip creating a SQL string when a JDBC target is available. The following variants are supported:
// Applies the loaded fixture set directly to a JDBC connection.
JFixtures.noConfig().load(fixturesPath).apply(connection);
// Applies compiled fixtures to a DataSource.
JFixtures.noConfig().load(fixturesPath).compile().apply(dataSource);
// Applies the rendered SQL form to a DataSource.
JFixtures.noConfig().load(fixturesPath).compile().toSql99().apply(dataSource);Use the SQL-string form when it is useful to log, inspect, or execute the generated script through an existing test utility. Use apply(...) when the test owns the connection or data source.
JFixtures emits cleanup statements before inserts. Configure a table when it needs a different cleanup strategy, prerequisites, timestamps, or primary-key behavior:
clean_tables: [orphaned]
tables:
orphaned:
applies_to: orphaned
clean_method: truncate
users:
applies_to: users
requires: [roles]
timestamps:
enabled: true
value: "sql:CURRENT_TIMESTAMP"clean_tables includes a table even if it has no fixture rows. clean_method: truncate produces a TRUNCATE TABLE statement; truncate_cascade is available for databases that support cascading truncation:
tables:
cascade_cleanup:
applies_to: users
clean_method: truncate_cascadeThe example produces TRUNCATE TABLE "users" CASCADE;. requires: [roles] ensures roles is inserted before users. Timestamp support adds the configured timestamp value to the standard audit columns of each matching row.
The feature fixtures in src/test/resources/yaml/features/new-features are executable documentation for additional capabilities:
| Need | Configuration or fixture example |
|---|---|
| Use UUID, string, or long primary keys | options.conf.yml and advanced.conf.yml configure id_generator or pk.type. |
| Reference a non-primary-key column | advanced.conf.yml maps comments.author_public_id to users.public_id; the value alice resolves the row label. |
| Inline reference to a specific column | users:alice:public_id in advanced/comments.yml. |
| Composite primary keys and references | composite-template.conf.yml. |
| Generate rows from a template | composite-template/users.yml, enabled with templates.enabled: true. |
| Many-to-many and polymorphic relations | associations.conf.yml. |
| Named configuration profiles | profiles.conf.yml, selected with withProfile("integration"). |
| SQL hooks before or after fixture processing | hooks.conf.yml plus the hooks directory. |
| YAML anchors and binary/date values | anchors/users.yml and values/events.yml. |
For example, select the default or a named profile after loading the same fixture data:
var fixtures = JFixtures
.withConfig(Paths.get("src/test/resources/yaml/features/new-features/profiles.conf.yml"))
.load(Paths.get("src/test/resources/yaml/features/new-features/profiles"));
String defaultSql = fixtures.withDefaultProfile().compile().toSql99().toString();
String integrationSql = fixtures.withProfile("integration").compile().toSql99().toString();The supplied configuration defines clean_method: none for the default profile and clean_method: truncate for the integration profile.
- Ensure every YAML file name matches the actual database table name. A wrong table name produces SQL for the wrong table.
- Add each foreign-key column to
refsand use the target row label as its YAML value. Do not use a label where the database expects an arbitrary numeric ID. - Specify
idonly when an explicit value is needed. Two labels with the same explicit primary key fail compilation withProcessorException. - Use
sql:for database functions and expressions. Without it,CURRENT_TIMESTAMP()is treated as a text value. - Render and inspect
toSql99().toString()when debugging ordering, cleanup, or reference resolution. - Keep fixture execution inside the test setup transaction when the test framework rolls transactions back; this prevents data from leaking between tests.
mvn clean install
or using the wrapper:
./mvnw clean install
http://localhost:8080/v1/api
mvn spring-boot:run
or using the wrapper:
./mvnw spring-boot:run
You can also run the application's main method from an IDE.