Back to Blog
Headshot of Bojan Ćorić

1 minute read

Spring Boot & Testcontainers

Bojan Ćorić

Sofware Engineer

Testcontainers is an open source library for providing throwaway, lightweight instances of anything that can run in a Docker container. Think of it as “Infrastructure-as-Code” specifically for your test suite. Instead of writing external scripts (like Bash or Docker Compose) to set up your environment before running tests, you define the environment inside your test code in your language of choice.

While it started in the Java world, Testcontainers is now a standardized protocol for testing across almost all major programming languages (Go, Python, C#, etc.).

The boilerplate era: Manual configuration

Before Spring Boot integrated tightly with Testcontainers, setting them up was cumbersome. Developers had to manually manage the container lifecycle and, more painfully, manually tell Spring Boot where to find these dynamically started containers.

// ❌ The verbose, manual way (avoid this now)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ManualConfigTest {

    static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:18.1-alpine3.23");

    @BeforeAll
    static void startContainers() {
        postgresContainer.start();
    }

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgresContainer::getUsername);
        registry.add("spring.datasource.password", postgresContainer::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void testDatabase() {
        userRepository.save(new User(null, "John", "Doe", "john.doe@mail.com"));
        List<User> users = userRepository.findAll();
        assertThat(users).isNotEmpty();
    }
}

JUnit support: @Testcontainers

The first major quality-of-life improvement came from the Testcontainers JUnit 5 support. By using the @Testcontainers and @Container annotations, the library took over the lifecycle management (starting and stopping containers).

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers // Enable testcontainers support
public class ManualConfigTest {

    @Container // JUnit manages lifecycle
    static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:18.1-alpine3.23");

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgresContainer::getUsername);
        registry.add("spring.datasource.password", postgresContainer::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void testDatabase() {
        userRepository.save(new User(null, "John", "Doe", "john.doe@mail.com"));
        List<User> users = userRepository.findAll();
        assertThat(users).isNotEmpty();
    }
}

The modern way: @ServiceConnection

Spring Boot 3.1 introduced a game-changer: @ServiceConnection. Spring Boot now understands standard containers (Postgres, Mongo, Kafka, Redis, etc.). When you annotate a container with @ServiceConnection, Spring Boot automatically finds the connection details (URL, username, password, ports) and injects them into the application context.

No more property overrides. No more manual config.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class ManualConfigTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:18.1-alpine3.23");

    @Autowired
    private UserRepository userRepository;

    @Test
    void testDatabase() {
        userRepository.save(new User(null, "John", "Doe", "john.doe@mail.com"));
        List<User> users = userRepository.findAll();
        assertThat(users).isNotEmpty();
    }
}

Conclusion

Testcontainers has evolved from a niche library requiring lengthy setup to an absolute necessity for modern software development. It is no longer just a “nice-to-have”; it is the new standard for building reliable, production-grade applications. Testcontainers eliminates the “it works on my machine” and “it works with the mock” classes of bugs entirely.

Ready to implement this? You don’t have to start from scratch. I’ve created a GitHub repository featuring “real-world” setups for Postgres, Redis, and Kafka.

I also included a bonus section demonstrating how to manually configure containers when @ServiceConnection isn’t available, giving you full control over any service you need to containerize.

Clone the repo and start experimenting: https://github.com/Corke123/testcontainers-demo

Headshot of Bojan Ćorić

Bojan Ćorić

Sofware Engineer

Bojan is a backend software developer at ProductDock, specializing in Java and Spring. He has been actively involved in developing robust and scalable backend systems for the past two years. His expertise in these technologies and his dedication to continuous learning make him a valuable asset in the field of software development.

Related posts.