Populate a database with TestContainers in a SpringBoot integration test
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
Introduction
Testcontainers lets Spring Boot integration tests run against a real disposable database instead of mocks, which improves confidence in repository and migration behavior. A common requirement is to preload test data before assertions run. The best approach combines container lifecycle setup, schema migration, and deterministic data seeding.
Configure Testcontainers with Spring Boot
Start by defining the database container in a test class and wiring dynamic properties.
This ensures Spring connects to the containerized database at runtime.
Seed Data with SQL Scripts
For deterministic setup, use SQL scripts with @Sql or initialization scripts.
Example seed-users.sql:
SQL scripts are explicit and easy to review.
Combine with Flyway or Liquibase Migrations
If your app uses migrations, run them in test startup so schema matches production.
application-test.yml example:
Then only seed business data in test scripts. Let migrations own schema changes.
This prevents drift between integration tests and real deployment schema.
Programmatic Seeding for Dynamic Scenarios
For tests that need custom rows per case, seed with repositories in @BeforeEach.
Programmatic setup is flexible but can hide data rules if overused. Use clear builders and helper methods.
Use Container Init Scripts for Shared Baselines
If many test classes need the same baseline schema or static lookup rows, attach an init script directly to the container.
Container init scripts run at startup and are good for shared fixtures. Keep test-specific inserts in method-level setup to preserve isolation.
Keep Parallel Tests Isolated
If your test runner executes classes in parallel, ensure each class has isolated data boundaries. One approach is unique schema names per class or deterministic cleanup in @BeforeEach.
Isolation prevents flaky assertions caused by cross-test row collisions.
Performance Tips for CI
Container startup can be expensive. Practical optimizations:
- reuse static container per test class,
- keep data sets minimal,
- avoid unnecessary app context reloads.
You can also use reusable containers in local dev, but keep CI deterministic and isolated.
If test order should not matter, clear and reseed data per test method.
Common Pitfalls
A common mistake is relying on in-memory database behavior while production uses PostgreSQL or MySQL. SQL dialect differences can hide bugs. Testcontainers helps avoid this mismatch.
Another issue is mixing schema creation and business data inserts in many test files, which creates maintenance overhead. Keep schema in migrations and data seeds in dedicated scripts.
Developers also forget cleanup between tests, causing data leakage across methods. Reset state each test or use transactional rollback strategy where appropriate.
Summary
- Use Testcontainers to run integration tests against a real disposable database.
- Wire container connection settings with
@DynamicPropertySource. - Seed deterministic data using SQL scripts or controlled programmatic setup.
- Run Flyway or Liquibase migrations in tests for schema consistency.
- Reset database state per test to keep results isolated and repeatable.
Related reading
- Populate data table from data reader
- Populate nested array in mongoose
- Possible to do a MySQL foreign key to one of two possible tables?
- Possibly consider using a shorter maxLifetime value - hikari connection pool spring boot
- Populating Spring @Value during Unit Test
- Postgres connection has been closed error in Spring Boot
- Possible to do a dry run validation of files?
- Practicing BDD with python

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.