TestContainers with SpringBoot and PostgreSQL

In this tutorial, we’ll walk through setting up a Spring Boot 3 application to use TestContainers for integration testing with PostgreSQL. TestContainers provides lightweight, disposable instances of databases and other services, which are essential for reliable and reproducible integration tests.

Prerequisites

Before you begin, ensure you have the following installed:

  • Java 17 or higher
  • Maven or Gradle
  • Docker

Finally, if you are new to TestContainers, we recommend checking this TestContainer tutorial.

Step 1: Add Dependencies

First, add the necessary dependencies to your pom.xml (for Maven) or build.gradle (for Gradle) file.

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

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

	<dependency>
		<groupId>org.postgresql</groupId>
		<artifactId>postgresql</artifactId>
		<scope>runtime</scope>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

Step 2: Create the Integration Test

In this example, we will not cover the base project structure which includes the Model, Controller and Repository Classes. You will find all the project elements in the github project, at the end of the article.

Next, create the CustomerServiceIntegrationTest class to test your Spring Boot application with Testcontainers.

package com.example.demo;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

import javax.transaction.Transactional;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
public class CustomerServiceIntegrationTest {

    @Autowired
    private CustomerRepository customerRepository;

    @LocalServerPort
    private Integer port;

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

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

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

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

    @Test
    void testFindAll() {
        Customer customer1 = new Customer("John", "Doe", "[email protected]");
        Customer customer2 = new Customer("Jane", "Doe", "[email protected]");
        customerRepository.save(customer1);
        customerRepository.save(customer2);

        List<Customer> list = customerRepository.findAll();
        assertNotNull(list, "The list of customers should not be null");
        assertEquals(2, list.size(), "The list should contain exactly 2 customers");

        assertTrue(list.stream().anyMatch(customer -> "John".equals(customer.getFirstName())),
                "The list should contain a customer with the first name 'John'");
        assertTrue(list.stream().anyMatch(customer -> "Jane".equals(customer.getFirstName())),
                "The list should contain a customer with the first name 'Jane'");
    }
}

Step 3: Explanation

  1. Annotations:
    • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT): This annotation tells Spring Boot to start an embedded server on a random port during the test.
    • @Transactional: This annotation ensures that each test method runs within a transaction that is rolled back at the end of the test, maintaining database state consistency.
  2. Testcontainers Setup:
    • PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"): This line creates a new PostgreSQL container using the postgres:16-alpine Docker image.
    • @BeforeAll: This method starts the PostgreSQL container before any tests run.
    • @AfterAll: This method stops the PostgreSQL container after all tests have run.
  3. Dynamic Property Source:
    • @DynamicPropertySource: This annotation dynamically sets the spring.datasource.url, spring.datasource.username, and spring.datasource.password properties to match the configuration of the running PostgreSQL container.
  4. Test Method:
    • The testFindAll method saves two Customer entities to the database and then retrieves them to verify that they were saved correctly.

Step 4: Run the Test

Run the test class using your IDE or a build tool like Maven or Gradle. The test will start a PostgreSQL container, set up the Spring Boot application to use it, and execute the test methods.

springboot testcontainer tutorial

Conclusion

Using Testcontainers with PostgreSQL in your Spring Boot integration tests allows you to create isolated, repeatable, and reliable tests that run against a real PostgreSQL database. This approach ensures that your tests are more robust and closer to the production environment compared to using in-memory databases like H2.

Source code for this article

Found the article helpful? if so please follow us on Socials
Twitter Icon       Facebook Icon       LinkedIn Icon       Mastodon Icon