services:
db: { image: postgres:16, environment: [POSTGRES_DB=app, POSTGRES_PASSWORD=pass] }
migrate:
image: liquibase/liquibase
depends_on: [db]
command: ["update", "--url=jdbc:postgresql://db:5432/app", "--changelog-file=/changelog.xml"]
app:
image: my-app
depends_on: [migrate]
Testcontainers for integration tests (Java):
@Testcontainers
class RepoIT {
@Container
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void dbProps(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
r.add("spring.flyway.enabled", () -> true); // Flyway runs against the container
r.add("spring.jpa.hibernate.ddl-auto", () -> "none");
}
}
2) CI: run migrations as a pipeline step
Typical PR check pipeline
- Start DB service (Compose service or CI “services”).
- Run migrations (Flyway/Liquibase CLI or Maven/Gradle plugin).
- Run tests (unit + integration).
- Optionally validate schema drift.
GitHub Actions sketch
services:
postgres:
image: postgres:16
env: { POSTGRES_DB: app, POSTGRES_PASSWORD: pass }
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- name: Wait for DB
run: until pg_isready -h localhost -p 5432; do sleep 1; done
- name: Run Flyway
run: ./mvnw -q -Dflyway.url=jdbc:postgresql://localhost:5432/app \
-Dflyway.user=postgres -Dflyway.password=pass flyway:migrate
- name: Tests
run: ./mvnw -q test
3) Integration & E2E environments: treat like prod, but disposable
- Dedicated DB per environment (e.g.,
app_test, app_e2e). - One-off “migrate” job before deploying the test app.
- Seed data via separate changelog folder (e.g.,
db/seed/) or fixtures loaded by the test harness. Keep seeds idempotent.
4) Reset strategies between test runs
- Flyway/Liquibase clean + migrate for quick wipe.
- Ephemeral DB: Testcontainers or throwaway schema name per run (e.g.,
schema_pr123_abcdef). - Snapshot/restore: initialize once, take a DB snapshot, restore per suite for speed (useful for heavy datasets).
5) Versioning and safety knobs
- Expand/Contract in tests too:
- Verify “expand” migrations allow old + new code to run.
- Run backfills as separate jobs you can test independently.
- Flyway
validateOnMigrate=true to catch drift.- Use
repair only to fix checksums on test DBs (never as a habit).
- Liquibase
liquibase updateSQL for dry-run SQL review in CI.diff/diffChangeLog to detect drift vs desired schema.
6) Seeding test data (cleanly)
- Prefer readable, isolated seeds:
- minimal “happy-path” records for integration tests,
- richer fixtures for E2E.
- Keep seeds separate from schema migrations (different path or tag) so prod doesn’t get test data.
7) Parallelism & race conditions
- If your tests run in parallel against one DB, you’ll fight locks and state leakage.
- Prefer one DB per test worker (Testcontainers, unique schema per worker, or a matrix of ports/DB names).
- Ensure only one migration runner acts on a given database at a time (lock via tool’s built-in table or by isolating per-worker DB).
8) Tooling quick refs
<plugin>
<groupId>org.flywaydb</groupId><artifactId>flyway-maven-plugin</artifactId>
<version>${flyway.version}</version>
<configuration>
<url>${flyway.url}</url>
<user>${flyway.user}</user>
<password>${flyway.password}</password>
<validateOnMigrate>true</validateOnMigrate>
</configuration>
</plugin>
Run: mvn -q flyway:clean flyway:migrate -Ptest
Liquibase (Gradle)
liquibase {
activities.register("testMigrate") {
this.arguments = mapOf(
"changelogFile" to "db/changelog-master.yaml",
"url" to System.getenv("TEST_DB_URL"),
"username" to "postgres",
"password" to "pass"
)
}
}
- Run:
./gradlew update -PliquibaseActivity=testMigrate
9) Suggested setups by environment
- Unit tests: no DB or use lightweight embedded (no migrations).
- Integration tests: Testcontainers + Flyway auto-migrate.
- Local manual testing:
docker compose up db migrate app; rerun migrate or clean+migrate as needed. - CI per PR: spin DB service → migrate → run tests; optionally run a dry-run (Liquibase
updateSQL) and schema-drift check. - Shared E2E env: pre-deploy “migrate” job; nightly refresh with snapshot.