TestContainers
SpringBoot
Integration Testing
Database
Java

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.

Practice system design

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.

java
1import org.junit.jupiter.api.Test;
2import org.springframework.boot.test.context.SpringBootTest;
3import org.springframework.test.context.DynamicPropertyRegistry;
4import org.springframework.test.context.DynamicPropertySource;
5import org.testcontainers.containers.PostgreSQLContainer;
6import org.testcontainers.junit.jupiter.Container;
7import org.testcontainers.junit.jupiter.Testcontainers;
8
9@SpringBootTest
10@Testcontainers
11class UserRepositoryIT {
12
13    @Container
14    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
15            .withDatabaseName("testdb")
16            .withUsername("test")
17            .withPassword("test");
18
19    @DynamicPropertySource
20    static void configure(DynamicPropertyRegistry registry) {
21        registry.add("spring.datasource.url", postgres::getJdbcUrl);
22        registry.add("spring.datasource.username", postgres::getUsername);
23        registry.add("spring.datasource.password", postgres::getPassword);
24    }
25
26    @Test
27    void contextLoads() {
28    }
29}

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.

java
1import org.junit.jupiter.api.Test;
2import org.springframework.beans.factory.annotation.Autowired;
3import org.springframework.test.context.jdbc.Sql;
4
5@Sql(scripts = "/sql/seed-users.sql")
6class UserRepositoryIT {
7
8    @Autowired
9    UserRepository repo;
10
11    @Test
12    void findsSeededUser() {
13        var user = repo.findByEmail("[email protected]");
14        assert(user.isPresent());
15    }
16}

Example seed-users.sql:

sql
INSERT INTO users(id, email, name) VALUES (1, '[email protected]', 'Ava');
INSERT INTO users(id, email, name) VALUES (2, '[email protected]', 'Liam');

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:

yaml
1spring:
2  flyway:
3    enabled: true
4  jpa:
5    hibernate:
6      ddl-auto: validate

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.

java
1import org.junit.jupiter.api.BeforeEach;
2import org.springframework.beans.factory.annotation.Autowired;
3
4class UserRepositoryIT {
5
6    @Autowired
7    UserRepository repo;
8
9    @BeforeEach
10    void seed() {
11        repo.deleteAll();
12        repo.save(new User(null, "[email protected]", "Ava"));
13        repo.save(new User(null, "[email protected]", "Noah"));
14    }
15}

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.

java
1@Container
2static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
3        .withDatabaseName("testdb")
4        .withUsername("test")
5        .withPassword("test")
6        .withInitScript("sql/init.sql");

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
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track 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.

Practice system design

All Rights Reserved.