Best Way to Use Testcontainers in Spring Boot Tests: @TestConfiguration & ServiceConnection

This article demonstrates the optimal way to integrate Testcontainers with Spring Boot tests using @TestConfiguration, @ServiceConnection, and @RestartScope for reusable Neo4j containers, avoiding dual lifecycle management, and shows how to extend this to dev services with Testcontainers Cloud fixed port mapping.

Linyb Geek Road
Linyb Geek Road
Linyb Geek Road
Best Way to Use Testcontainers in Spring Boot Tests: @TestConfiguration & ServiceConnection

When using the Testcontainers JUnit 5 extension with Spring Boot tests, two systems attempt to manage container lifecycles, which is not ideal. The solution is to use @TestConfiguration.

Example Application

A Spring Boot application using Spring Data Neo4j:

import java.util.List;
import java.util.UUID;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableNeo4jRepositories(considerNestedRepositories = true)
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Node
    public record Movie(@Id @GeneratedValue(GeneratedValue.UUIDGenerator.class) String id, String title) {
        Movie(String title) {
            this(UUID.randomUUID().toString(), title);
        }
    }

    interface MovieRepository extends Neo4jRepository<Movie, String> {
    }

    @RestController
    static class MovieController {
        private final MovieRepository movieRepository;

        public MovieController(MovieRepository movieRepository) {
            this.movieRepository = movieRepository;
        }

        @GetMapping("/movies")
        public List<Movie> getMovies() {
            return movieRepository.findAll();
        }
    }
}

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

Using @TestConfiguration

@TestConfiguration

provides extra test configuration. Compared to @Configuration, it has two advantages:

It does not block auto-detection of @SpringBootConfiguration.

Unless it is a static inner class of the test class, it must be explicitly imported.

The following configuration class defines a @Bean method annotated with @ServiceConnection that returns a reusable Neo4jContainer:

import java.util.Map;

import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.Neo4jContainer;

@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfig {

    @Bean
    @ServiceConnection
    @RestartScope
    public Neo4jContainer<?> neo4jContainer() {
        return new Neo4jContainer<>("neo4j:5")
                .withLabels(Map.of("com.testcontainers.desktop.service", "neo4j"))
                .withReuse(true);
    }
}

The container is marked reusable so it stays alive between test runs, making subsequent runs faster. The @ServiceConnection annotation provides enough context for Spring to rewire all Neo4j connections to this container.

Test Class

The test imports the configuration and uses the repository directly:

import static org.assertj.core.api.Assertions.assertThat;

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;

@SpringBootTest
@Import(ContainerConfig.class)
class MyApplicationTests {

    @Test
    void repositoryIsConnectedAndUsable(
            @Autowired MyApplication.MovieRepository movieRepository
    ) {
        var movie = movieRepository.save(new MyApplication.Movie("Barbieheimer"));
        assertThat(movie.id()).isNotNull();
    }
}

Why @RestartScope?

The @RestartScope annotation exists for three reasons:

Spring DevTools may be on the classpath and will restart the context when needed.

When the context restarts, the container would also restart, invalidating the reusable flag.

The annotation preserves the original bean across restarts.

Dev Services with SpringApplication.with

The new SpringApplication.with method allows augmenting auto-configuration with additional configuration. The same ContainerConfig can be used in a separate main class for development:

import org.springframework.boot.SpringApplication;

public class MyApplicationWithDevServices {

    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main)
                .with(ContainerConfig.class)
                .run(args);
    }
}

Starting this application makes requests to http://localhost:8080/movies work immediately against the Neo4j container.

Testcontainers Cloud Fixed Port Mapping

The label com.testcontainers.desktop.service=neo4j enables Testcontainers Cloud integration. The author configures fixed port mapping in ~/.config/testcontainers/services/neo4j.toml:

# This example selects neo4j instances and forwards port 7687 to 7687 on the client.
# Same for the Neo4j HTTP port
# Instances are found by selecting containers with label "com.testcontainers.desktop.service=neo4j".

# ports defines which ports to proxy.
# local-port indicates which port to listen on the client machine. System ports (0 to 1023) are not supported.
# container-port indicates which port to proxy. If unset, container-port will default to local-port.
ports = [
  {local-port = 7687, container-port = 7687},
  {local-port = 7474, container-port = 7474}
]

This allows accessing the Neo4j instance at well-known ports. The author demonstrates inserting data via cypher-shell and retrieving it through the application:

# Use Cypher-Shell to create some data
cypher-shell -uneo4j -ppassword "CREATE (:Movie {id: randomUuid(), title: 'Dune 2'})"
# 0 rows
# ready to start consuming query after 15 ms, results consumed after another 0 ms
# Added 1 nodes, Set 2 properties, Added 1 labels

# Request the data from the application running with dev services
http localhost:8080/movies

# HTTP/1.1 200
# Connection: keep-alive
# Content-Type: application/json
# Date: Thu, 27 Jul 2023 13:32:58 GMT
# Keep-Alive: timeout=60
# Transfer-Encoding: chunked
#
# [
#     {
#         "id": "824ec97e-0a97-4516-8189-f0bf5eb215fe",
#         "title": "Dune 2"
#     }
# ]

The author concludes that this approach is explicit, concise, and uses only Spring annotations, making it the preferred way to integrate Testcontainers in Spring Boot tests.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TestingSpring BootNeo4jTestcontainers@RestartScope@ServiceConnection@TestConfigurationTestcontainers Cloud
Linyb Geek Road
Written by

Linyb Geek Road

Tech notes

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.