Tuesday, November 18, 2025

🚀 Using Testcontainers for Real Integration Tests (Spring Boot + JUnit 5)

 Modern applications rely on multiple external systems — relational databases, messaging services, cloud APIs, and more. Unit tests alone can’t reliably validate these integrations.

Testcontainers has become the de-facto solution for running lightweight, disposable Docker containers directly from your JUnit 5 tests. This enables full integration testing without managing local databases or emulators manually.

In this post, walk you through a generic, reusable setup for:

✔ Spring Boot (3.x)
✔ JUnit 5
✔ PostgreSQL & Oracle containers
✔ Google Pub/Sub emulator
✔ Multi-datasource testing
✔ GitHub Actions or any CI environment

 


1️⃣ Why Testcontainers?

Testcontainers lets you spin up real infrastructure inside your tests:

  • Real PostgreSQL and Oracle Free instances

  • Real Pub/Sub emulator

  • Real networking

  • Real JDBC connections

All containers are created and torn down automatically → zero manual infrastructure.

 It works on:

  • Docker Desktop

  • Podman on Windows (podman machine start)

  • Linux / macOS

  

2️⃣ Dependencies (pom.xml)

Add the core Testcontainers modules:

<!-- Core Testcontainers --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <!-- Database Containers --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>oracle-free</artifactId> <scope>test</scope> </dependency> <!-- GCP Emulator --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>gcloud</artifactId> <scope>test</scope> </dependency>

 

3️⃣ Test Profile (application-test.yml)

Create a fully isolated test profile:

spring: cloud: gcp: pubsub: enabled: true emulator-host: localhost:8085 application: name: "Integration Test Suite" server: port: 0 logging: request: shouldLog: true includePayload: true datasource: driver-class-name: oracle.jdbc.OracleDriver maximum-pool-size: 5 second-datasource: driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: create-drop
 
 Adapt the property names to your own project. 

 

4️⃣ Base Testcontainers Class

This reusable class bootstraps all containers once per test suite:

@ActiveProfiles("test") @Testcontainers public abstract class BaseIntegrationTest { @Container static final OracleContainer ORACLE = new OracleContainer("gvenzl/oracle-free:23-slim-faststart") .withUsername("testuser") .withPassword("testpass") .withDatabaseName("testdb"); @Container static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("testpass"); @Container static final GenericContainer<?> PUBSUB = new GenericContainer<>("gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators") .withCommand("gcloud", "beta", "emulators", "pubsub", "start", "--host-port=0.0.0.0:8085") .withExposedPorts(8085); @DynamicPropertySource static void registerProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", ORACLE::getJdbcUrl); registry.add("spring.datasource.username", ORACLE::getUsername); registry.add("spring.datasource.password", ORACLE::getPassword); registry.add("wsj-datasource.jdbc-url", POSTGRES::getJdbcUrl); registry.add("wsj-datasource.username", POSTGRES::getUsername); registry.add("wsj-datasource.password", POSTGRES::getPassword); String emulatorHost = PUBSUB.getHost() + ":" + PUBSUB.getMappedPort(8085); registry.add("spring.cloud.gcp.pubsub.emulator-host", () -> emulatorHost); } }
 

Every integration test now has:

  • A running Oracle DB

  • A running PostgreSQL DB

  • A running Pub/Sub emulator

No external services. No mocks.

5️⃣ Example Integration Test

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class UserEventServiceIT extends BaseIntegrationTest { @Autowired private UserEventService service; @Autowired private UserEventRepository repository; @BeforeEach void before() { repository.deleteAll(); } @Test @DisplayName("Should persist event correctly") void testSaveEvent() { service.saveEvent("test-id", "EventType"); Optional<UserEvent> saved = repository.findFirstByEventIdOrderByCreatedDesc("test-id"); assertThat(saved).isPresent(); assertThat(saved.get().getEventId()).isEqualTo("test-id"); } }

Readable, deterministic, and powered by real infrastructure.

 

6️⃣ Optional: .testcontainers.properties

At project root:

docker.client.strategy=org.testcontainers.dockerclient.NpipeSocketClientProviderStrategy reuse.enable=true

Useful for Windows + Podman, and for faster local test cycles.

7️⃣ Dockerfile Tip for Skip Tests

For Spring Boot builds:

RUN mvn clean package -DskipTests=true -DskipITs=true -Dmaven.test.skip=true

 

8️⃣ CI/CD Considerations

In GitHub Actions or other CI systems:

  • Testcontainers automatically pulls containers

  • You don’t need database dumps or SQL files

🔚 Final Thoughts

Integration testing is often neglected due to infrastructure complexity.

With Testcontainers, this barrier disappears.

You get:

✔ Realistic tests
✔ Production-like behavior
✔ Zero manual environment setup
✔ Repeatability across machines and CI

Whether your application uses relational databases, cloud services, messaging systems, 

or all of them — Testcontainers makes integration testing simple, fast, and reliable.

 





🚀 Using Testcontainers for Real Integration Tests (Spring Boot + JUnit 5)

 Modern applications rely on multiple external systems — relational databases, messaging services, cloud APIs, and more. Unit tests alone ca...