Skip to content

Java API and JUnit tests

New in Migrax 0.10.0.

Migrax can run from your own Java code, and its JUnit 5 extension can check in a unit test that your migrations still match your entities.

Test that migrations match the entities

Add migrax-junit as a test dependency:

<dependency>
  <groupId>org.migrax</groupId>
  <artifactId>migrax-junit</artifactId>
  <version>0.17.1</version>
  <scope>test</scope>
</dependency>

Then put @MigraxTest on a test class:

import org.migrax.junit.MigraxTest;

@MigraxTest
class BookRepositoryTest {

  @Test
  void savesBooks(DataSource dataSource) throws Exception {
    // The database already has every migration applied.
  }
}

Before the tests run, Migrax creates the test database, applies every migration from src/main/resources/db/migration and compares the result with your entities. If they differ, every test in the class fails with a list of the differences:

The migrated schema does not match the entities (Hibernate 6.6.13.Final mapping):
  - book.isbn: column is missing from the database
Generate a migration for the entity changes (migrax generate), or fix the migration that differs.

This catches an entity that changed without a new migration, and a migration that was edited by hand and no longer creates what the entities need. The test is an ordinary JUnit test, so it runs in mvn test and in your CI like every other test.

What is compared

The entities are read the same way migrax generate reads them: through Hibernate, EclipseLink or OpenJPA when the project has one, otherwise from the JPA annotations. Every table, column, primary key, foreign key and check constraint the entities need must be in the database, with a matching type and nullability.

The database may contain more than the entities need: tables that no entity maps, and nullable columns that no entity maps, are fine. Other code may use them. A NOT NULL column without a default that no entity maps is reported, because inserting an entity would fail.

Which database

By default the test runs on a new H2 database in memory, which needs H2 on the test classpath. This works when your migrations are written for H2.

For the database you use in production, mark a static field with @MigraxDatabase. The field can hold a DataSource or a Testcontainers JDBC container. A container that isn't running is started:

@MigraxTest
class OrderRepositoryTest {

  @MigraxDatabase
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");

  @Test
  void findsOrders(DataSource dataSource) { ... }
}

You can also give a JDBC URL: @MigraxTest(url = "jdbc:postgresql://localhost:5432/test", user = "test", password = "test"). Use a database that only tests use.

Settings

Setting Default Meaning
entities migrax.package, else the Maven groupId Package that holds the entities
locations db/migration Migration folders on the classpath; filesystem:<path> for folders on disk
javaPackage db.migration Package of JavaMigration classes
validate true Compare the migrated schema with the entities
rollbacks false Also test every rollback script (see below)
schemas Schemas to migrate one after another
env Environment: migrations marked -- migrax:env <name> run only there
url, user, password The test database, when no field is marked @MigraxDatabase

The naming of tables and columns is the one the migrax command uses for your project. It is recorded in .migrax/snapshot.json.

Test the rollback scripts too

With @MigraxTest(rollbacks = true), Migrax rolls each migration back with its rollback script, applies it again and checks that the schema is the same as before. A broken rollback script fails the tests and is named:

Rollback scripts: failed: 0002_add_isbn.sql fails when applied again after its rollback script ran: ...

This is the same check migrax verify runs.

Parameters

Test methods, constructors and @BeforeEach methods can ask for these as parameters:

Type What it is
javax.sql.DataSource The migrated test database
org.migrax.api.Migrax The Java API for this database (below)
org.migrax.api.Migrax.MigrateResult How many migrations were applied

The database is migrated once per test class. @Nested classes share it with the class around them.

Run Migrax from Java

The migrax library (org.migrax:migrax) has a Java API. Use it when your application starts some other way than the framework integrations, or in tools of your own:

import org.migrax.api.Migrax;

Migrax migrax = Migrax.configure()
    .dataSource(dataSource)
    .locations("db/migration")
    .load();

int applied = migrax.migrate().applied();
Method What it does
migrate() Applies the pending migrations; returns how many, per schema
status() The state of every migration (applied, pending, failed, changed, ...) without changing anything; upToDate() tells if nothing is pending or wrong
rollback(steps) Rolls back the newest migrations with their rollback scripts; returns their names
rollbackTo(version) Rolls back every migration after version
validate() Compares the entities with the database, as described above
testRollbacks() Tests every rollback script on the database. Rolling back drops data, so use a test database

The builder takes the same settings as the startup integrations:

Builder method Meaning
dataSource(ds) or url(url, user, password) The database (required)
locations(...) Migration folders: classpath folders, or filesystem:<path>
javaPackage(name) Package of JavaMigration classes (default db.migration)
javaMigrations(...) JavaMigration objects to run as well
schemas(...) Schemas to migrate one after another
environment(name) Environment for -- migrax:env migrations
placeholder(name, value) Values for ${name} placeholders
lockTimeout(duration) How long to wait while another instance migrates
resume(true) Re-run a failed migration marked -- migrax:resume-safe
entities(package), naming(name) For validate(): the entity package and naming
classLoader(loader) Where migrations and JDBC drivers are found
listener(...) Listeners for metrics and tracing (Metrics and tracing)
settings(map) migrax.* settings, as in application.properties

Each method call opens its own connection and closes it again.