1. Overview

In this short article, we’ll learn about the Testcontainers JDBC support, and we’ll compare two different ways of spinning up Docker containers in our tests.

Initially, we’ll manage the Testcontainer’s lifecycle programmatically. After that, we’ll simplify this setup through a single configuration property, leveraging the framework’s JDBC support.

2. Managing Testcontainer Lifecycle Manually

Testcontainers is a framework that provides lightweight disposable Docker containers for testing. We can use it to run tests against real services such as databases, message queues, or web services without mocks or external dependencies.

Let’s imagine we want to use Testcontainers to verify the interaction with our PostgreSQL database. Firstly, we’ll add the testcontainers dependencies to our pom.xml:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.8</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.8</version>
    <scope>test</scope>
</dependency>

After that, we’ll have to manage the container’s lifecycle, following a few simple steps:

  • Create a container object
  • Start the container before all the tests
  • Configure the application to connect with the container
  • Stop the container at the end of the tests

We can implement these steps ourselves using JUnit5 and Spring Boot annotations such as @BeforeAll, @AfterAll, and @DynamicPropertyRegistry:

@SpringBootTest
class FullTestcontainersLifecycleLiveTest {
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16-alpine")
      .withDatabaseName("test-db");

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

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

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }

    // tests
}

Even though this solution allows us to customize specific lifecycle phases, it requires a complex setup. Luckily, the framework provides a convenient solution to launch containers and communicate with them through JDBC using minimal configuration.

3. Using the Testcontainers JDBC Driver

Testcontainers will automatically start a Docker container hosting our database when we use their JDBC driver. To do this, we need to update the JDBC URL for the test execution, and use the pattern: “jdbc:tc:::///.

Let’s use this syntax to update spring.datasource.url in our test:

spring.datasource.url: jdbc:tc:postgresql:16-alpine:///test-db

Needless to say, this property can be defined in a dedicated configuration file or in the test itself through the @SpringBootTest annotation:

@SpringBootTest(properties =
  "spring.datasource.url: jdbc:tc:postgresql:16-alpine:///test-db"
)
class CustomTestcontainersDriverLiveTest {
    @Autowired
    HobbitRepository theShire;

    @Test
    void whenCallingSave_thenEntityIsPersistedToDb() {
        theShire.save(new Hobbit("Frodo Baggins"));

        assertThat(theShire.findAll())
          .hasSize(1).first()
          .extracting(Hobbit::getName)
          .isEqualTo("Frodo Baggins");
    }
}

As we can notice, we no longer have to manually handle the PostgreSQL container’s lifecycle. Testcontainers handle this complexity, allowing us to focus solely on the tests at hand.

4. Conclusion

In this brief tutorial, we explored different ways to spin up a Docker Container and connect to it via JDBC. Firstly, we manually created and started the container, and connected it with the application. This solution requires more boilerplate code but allows specific customizations. On the other hand, when we used the custom JDBC driver from Testcontainers, we achieved the same setup with just a single line of configuration.

As always, the complete code used in this article is available over on GitHub.