Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Tests on a real PostgreSQL: Testcontainers

So far every test has run on in-memory H2, while the server runs PostgreSQL. The databases are similar but not the same: a native SQL query, a column type or a migration may pass on H2 and break on PostgreSQL. Testcontainers is a library that starts a real database in a Docker container before a test and removes it afterwards. You do not have to install PostgreSQL on your computer, but you do need a running Docker (on Windows and macOS — Docker Desktop).

Dependencies

On start.spring.io these are the Testcontainers and PostgreSQL Driver modules. Three test dependencies appear in pom.xml:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-postgresql</artifactId>
    <scope>test</scope>
</dependency>

No versions are needed — Spring Boot sets them (Boot 4.1 uses Testcontainers 2). You also need the org.postgresql:postgresql driver and the flyway-database-postgresql module — the same ones as in the lessons on connecting a database and on Flyway. If you have not added them yet, add them now: the server will need them anyway.

A container as a bean

The generator puts this class into the tests, only with the postgres:latest image. It is better to pin the same version as on the server:

// src/test/java/com/example/blog/TestcontainersConfiguration.java
package com.example.blog;
 
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.postgresql.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
 
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {
 
    @Bean
    @ServiceConnection
    PostgreSQLContainer postgresContainer() {
        return new PostgreSQLContainer(DockerImageName.parse("postgres:18"));
    }
}

The container here is an ordinary Spring bean: it starts with the test context and stops after all the tests. @ServiceConnection is the key part of the lesson: Spring Boot asks the container for its address, port, user name and password and connects the DataSource and Flyway to them. You do not write spring.datasource.url; the address from application.yaml is replaced.

The test

// src/test/java/com/example/blog/PostgresTests.java
package com.example.blog;
 
import static org.assertj.core.api.Assertions.assertThat;
 
import java.time.Instant;
 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.Transactional;
 
@SpringBootTest
@Import(TestcontainersConfiguration.class)
@Transactional
class PostgresTests {
 
    @Autowired
    private PostRepository posts;
 
    @Autowired
    private UserRepository users;
 
    @Test
    void searchWorksOnPostgres() {
        User anna = users.save(new User("anna", "{noop}changeme"));
        posts.save(new Post("Hello, PostgreSQL", "Text", anna, Instant.now()));
 
        assertThat(posts.search("postgresql")).hasSize(1);
        assertThat(posts.findLatest(5)).hasSize(1);
    }
}

@Import brings in the configuration with the container. We do not enable the test profile with H2 here. The test checks what depends on the database: Flyway migrations, case-insensitive search and native SQL with limit.

Running it

./mvnw test -Dtest=PostgresTests

On the first run Docker downloads the postgres:18 image, which takes a minute. The test log shows Testcontainers lines about starting the container, then the usual JUnit result. The other tests still run on H2 and do not need Docker.

The application with a database in a container

The generator creates one more class:

// src/test/java/com/example/blog/TestBlogApplication.java
package com.example.blog;
 
import org.springframework.boot.SpringApplication;
 
public class TestBlogApplication {
 
    public static void main(String[] args) {
        SpringApplication.from(BlogApplication::main).with(TestcontainersConfiguration.class).run(args);
    }
}

It starts the whole application with the same PostgreSQL in a container: ./mvnw spring-boot:test-run. This is handy for checking the blog in a browser on a real database without installing one.

Common mistake

Docker is not installed or not running. The test fails before its first line:

ERROR ... DockerClientProviderStrategy : Could not find a valid Docker environment. Please check configuration.
Could not find a valid Docker environment for Testcontainers.
    - Ensure a Docker-compatible container engine is installed and running.
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0 <<< FAILURE! -- in com.example.blog.PostgresTests
Caused by: java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration

Start Docker Desktop, wait until it has fully loaded, then repeat the command. You can check Docker on its own: docker info should print information about the server, not an error.

Thanks for reading!

Donate to the site — we work hard for you.