Spring Boot 3.1 introduced support for Testcontainers by adding the new ConnectionDetails and ServiceConnection abstractions. If you haven’t read about it yet, check out Spring Boot Application Testing and Development with Testcontainers.
The following Testcontainers container abstractions were already supported as part of Spring Boot 3.1:
- CassandraContainer
- CouchbaseContainer
- ElasticsearchContainer
- GenericContainer using Redis or openzipkin/zipkin
- JdbcDatabaseContainer
- KafkaContainer
- MongoDBContainer
- MariaDBContainer
- MSSQLServerContainer
- MySQLContainer
- Neo4jContainer
- OracleContainer
- PostgreSQLContainer
- RabbitMQContainer
- RedpandaContainer
Spring Boot 3.2.0 supports:
- GenericContainer using symptoma/activemq or otel/opentelemetry-collector-contrib
- OracleContainer (oracle-free)
- PulsarContainer
Read more about What’s new with Testcontainers in Spring Boot 3.2.0.
Testcontainers allows you to write integration tests and be confident that the integration between the application in development and the service to interact with works as intended. WireMock helps by simulating API dependencies.
An API dependency, in this context, refers to an external service that your application relies on for specific functionality or data. In our case, we will have GitHub API as an API dependency. It is often not easy to run such dependencies by yourself. Nowadays, WireMock provides its own WireMock implementation.
Building your own Spring Boot auto-configuration is a common use case when you want to implement an integration that can be shared across teams. In the following example, we will write a simple auto-configuration to integrate with the GitHub GraphQL API.
First, let’s declare WireMock’s Testcontainers dependency:
<dependency>
<groupId>org.wiremock.integrations.testcontainers</groupId>
<artifactId>wiremock-testcontainers-module</artifactId>
<version>1.0-alpha-13</version>
</dependency>
Also, let’s add the maven-dependency-plugin
:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>compile</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.github.nilwurtz</groupId>
<artifactId>wiremock-graphql-extension</artifactId>
<version>0.7.1</version>
<classifier>jar-with-dependencies</classifier>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/test-wiremock-extension</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
The plugin will download the WireMock GraphQL Extension, which will be used when writing tests.
Building the AutoConfiguration
Nowadays, the following approach is the recommended way to provide custom configuration options for your applications. @ConfigurationProperties
will allow you to inject values for github.url
and github.token
via environment variables, system properties, or properties in the application.properties
or application.yaml
files and hence make full use of Spring’s built-in configuration capabilities.
@ConfigurationProperties(prefix = "github")
public record GHProperties(String url, String token) {
}
The GHProperties
bean can be injected wherever it is needed by using @EnableConfigurationProperties
, as you can see in the AutoConfiguration below.
@AutoConfiguration
@EnableConfigurationProperties(GHProperties.class)
public class GHAutoConfiguration {
@Bean
GraphQlClient ghGraphQlClient(GHProperties properties, WebClient.Builder webClientBuilder) {
var githubBaseUrl = properties.url();
var authorizationHeader = "Bearer %s".formatted(properties.token());
return HttpGraphQlClient
.builder(webClientBuilder.build())
.url(githubBaseUrl + "/graphql")
.header("Authorization", authorizationHeader)
.build();
}
}
Now, let’s register the AutoConfiguration in src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
:
com.example.testcontainerswiremockexample.GHAutoConfiguration
GHAutoConfiguration
will provide the infrastructure and can be packaged and distributed independently. Then, the JAR can be used in any project and can be used as part of our application.
Using the AutoConfiguration
Let’s create a GHService
class and use GraphQlClient
, which now will be injected automatically thanks to the auto-configuration implemented previously.
@Service
public class GHService {
private final GraphQlClient graphQlClient;
public GHService(GraphQlClient ghGraphQlClient) {
this.ghGraphQlClient = ghGraphQlClient;
}
public Mono<GitHubResponse> getStats(Map<String, Object> variables) {
return this.graphQlClient.documentName("githubStats")
.operationName("Stats")
.variables(variables)
.retrieve("repository")
.toEntity(GitHubResponse.class);
}
}
The GraphQL query will use the document githubStats.graphql
, and the operation will return a GitHubResponse
entity:
public record GitHubResponse(Issues issues, PullRequests pullRequests, Stargazers stargazers, Watchers watchers, Forks forks) {
record Issues(int totalCount) {}
record PullRequests(int totalCount) {}
record Stargazers(int totalCount) {}
record Watchers(int totalCount) {}
record Forks(int totalCount) {}
}
Now, let’s add the content below in src/main/java/graphql-documents/githubStats.graphql
:
query Stats($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
issues(states: OPEN) {
totalCount
}
pullRequests(states: OPEN) {
totalCount
}
stargazers {
totalCount
} watchers {
totalCount
} forks {
totalCount
}
}
}
Testing with @DynamicPropertySource
Thanks to @DynamicPropertySource
, we can specify the properties defined in GHProperties
at runtime, so the tests will contain the proper configuration to connect to a WireMock instance running via Testcontainers.
@SpringBootTest
@Testcontainers
class TestcontainersWiremockExampleApplicationTests {
private static final Logger LOGGER = LoggerFactory.getLogger("wiremock");
@Container
static WireMockContainer wireMock = new WireMockContainer("wiremock/wiremock:3.2.0-alpine")
.withMapping("graphql", TestcontainersWiremockExampleApplicationTests.class, "graphql-resource.json")
.withExtensions("graphql",
List.of("io.github.nilwurtz.GraphqlBodyMatcher"),
List.of(Paths.get("target", "test-wiremock-extension", "wiremock-graphql-extension-0.7.1-jar-with-dependencies.jar").toFile()))
.withFileFromResource("testcontainers-java.json", TestcontainersWiremockExampleApplicationTests.class, "testcontainers-java.json")
.withLogConsumer(new Slf4jLogConsumer(LOGGER));
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("github.url", wireMock::getBaseUrl);
registry.add("github.token", () -> "test");
}
@Autowired
private GHService ghService;
@Test
void contextLoads() {
var variables = Map.<String, Object>of("owner", "testcontainers", "name", "testcontainers-java");
StepVerifier.create(this.ghService.getStats(variables))
.expectNext(new GitHubResponse(
new GitHubResponse.Issues(385),
new GitHubResponse.PullRequests(90),
new GitHubResponse.Stargazers(6560),
new GitHubResponse.Watchers(142),
new GitHubResponse.Forks(1295)))
.verifyComplete();
}
}
The test defines the WireMockContainer
along with the mapping, the extension, and the response. It also configures the log in order to get feedback from WireMock in case the request doesn’t match.
Additional resources are also needed for our integration tests: src/test/resources/com/example/testcontainerswiremockexamples/TestcontainersWiremockExampleApplicationTests/graphql-resource.json
:
{
"request": {
"method": "POST",
"url": "/graphql",
"headers": {
"Authorization": {
"contains": "Bearer"
}
},
"bodyPatterns": [
{
"equalToJson": "{\"query\":\"query Stats($owner: String!, $name: String!) {\\n repository(owner: $owner, name: $name) {\\n issues(states: OPEN) {\\n totalCount\\n }\\n pullRequests(states: OPEN) {\\n totalCount\\n }\\n stargazers {\\n totalCount\\n } watchers {\\n totalCount\\n } forks {\\n totalCount\\n }\\n }\\n}\", \"operationName\": \"Stats\", \"variables\":{\"owner\":\"testcontainers\",\"name\":\"testcontainers-java\"}}"
}
]
},
"response": {
"status": 200,
"bodyFileName": "testcontainers-java.json",
"headers": {
"Content-Type": "application/json"
}
}
}
And: src/test/resources/com/example/testcontainerswiremockexamples/TestcontainersWiremockExampleApplicationTests/testcontainers-java.json
:
{
"data": {
"repository": {
"issues": {
"totalCount": 385
},
"pullRequests": {
"totalCount": 90
},
"stargazers": {
"totalCount": 6560
},
"watchers": {
"totalCount": 142
},
"forks": {
"totalCount": 1295
}
}
}
}
Adding ConnectionDetails support
Let’s modernize our auto-configuration by adding ConnectionDetails
. In this case, the same parameters as in GHProperties
are being exposed, because those are needed to connect with the service we are simulating with WireMock.
public interface GHConnectionDetails extends ConnectionDetails {
String url();
String token();
}
GHConnectionsDetails
will provide two implementations; the first will still rely on properties, and the second will be shown later.
class PropertiesGHConnectionDetails implements GHConnectionDetails {
private final GHProperties properties;
public PropertiesGHConnectionDetails(GHProperties properties) {
this.properties = properties;
}
@Override
public String url() {
return this.properties.url();
}
@Override
public String token() {
return this.properties.token();
}
}
Then, as part of the auto-configuration, a GHConnectionDetails
bean will be created when no other bean is provided. PropertiesGHConnectionDetails
will be the default implementation in case no other is provided.
@AutoConfiguration
@EnableConfigurationProperties(GHProperties.class)
public class GHAutoConfiguration {
@Bean
@ConditionalOnMissingBean(GHConnectionDetails.class)
GHConnectionDetails ghConnectionDetails(GHProperties properties) {
return new PropertiesGHConnectionDetails(properties);
}
@Bean
GraphQlClient ghGraphQlClient(GHConnectionDetails connectionDetails, WebClient.Builder webClientBuilder) {
var githubBaseUrl = connectionDetails.url();
var authorizationHeader = "Bearer %s".formatted(connectionDetails.token());
return HttpGraphQlClient
.builder(webClientBuilder.build())
.url(githubBaseUrl + "/graphql")
.header("Authorization", authorizationHeader)
.build();
}
}
Adding WireMock Testcontainers for Spring Boot’s ServiceConnection
Before continuing, let’s declare spring-boot-testcontainers
as a dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
</dependency>
At this point, there is only one implementation so far, which is PropertiesGHConnectionDetails
. But, let’s add a second implementation for GHConnectionDetails
, which will rely on WireMockContainer
.
class WireMockContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory<WireMockContainer, GHConnectionDetails> {
WireMockContainerConnectionDetailsFactory() {
}
protected GHConnectionDetails getContainerConnectionDetails(ContainerConnectionSource<WireMockContainer> source) {
return new WireMockContainerConnectionDetails(source);
}
private static final class WireMockContainerConnectionDetails extends ContainerConnectionDetailsFactory.ContainerConnectionDetails<WireMockContainer> implements GHConnectionDetails {
private WireMockContainerConnectionDetails(ContainerConnectionSource<WireMockContainer> source) {
super(source);
}
@Override
public String url() {
return getContainer().getBaseUrl();
}
@Override
public String token() {
return "test-token";
}
}
}
Let’s register WireMockContainerConnectionDetailsFactory
in src/main/resources/META-INF/spring.factories
:
org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.testcontainerswiremockexample.WireMockContainerConnectionDetailsFactor
Using WireMock Testcontainers ServiceConnection
Now, we can remove the manual mapping of properties for the WireMockContainer
and rely directly on the @ServiceConnection
:
@SpringBootTest
@Testcontainers
class TestcontainersWiremockExampleApplicationTests {
private static final Logger LOGGER = LoggerFactory.getLogger("wiremock");
@Container
@ServiceConnection
static WireMockContainer wireMock = new WireMockContainer("wiremock/wiremock:3.2.0-alpine")
.withMapping("graphql", TestcontainersWiremockExampleApplicationTests.class, "graphql-resource.json")
.withExtensions("graphql",
List.of("io.github.nilwurtz.GraphqlBodyMatcher"),
List.of(Paths.get("target", "test-wiremock-extension", "wiremock-graphql-extension-0.7.1-jar-with-dependencies.jar").toFile()))
.withFileFromResource("testcontainers-java.json", TestcontainersWiremockExampleApplicationTests.class, "testcontainers-java.json")
.withLogConsumer(new Slf4jLogConsumer(LOGGER));
@Autowired
private GHService ghService;
@Test
void contextLoads() {
var variables = Map.<String, Object>of("owner", "testcontainers", "name", "testcontainers-java");
StepVerifier.create(this.ghService.getStats(variables))
.expectNext(new GitHubResponse(
new GitHubResponse.Issues(385),
new GitHubResponse.PullRequests(90),
new GitHubResponse.Stargazers(6560),
new GitHubResponse.Watchers(142),
new GitHubResponse.Forks(1295)))
.verifyComplete();
}
}
And, just like that, adding support to connect with those external services during testing or development can be achieved by Testcontainers. You can find the source code on GitHub.
Conclusion
Using Testcontainers and WireMock for simulating API behavior during testing and development is a powerful and efficient approach that offers numerous benefits to software development teams.
This combination enables engineering teams to create a controlled and isolated testing environment that closely mirrors the real-world API interactions, fostering robust and reliable testing practices. And, with Spring Boot’s ConnectionDetails and ServiceConnection abstractions, the Testcontainers integration and the developer experience would be seamless.
Learn more
- Subscribe to the Docker Newsletter.
- Get the latest release of Docker Desktop.
- New to Docker? Get started.
- Have questions about Testcontainers? Connect on the Testcontainers Slack.
- Get started with Testcontainers Cloud by creating a free account.
- Learn about Testcontainers best practices.
- Get started with the Testcontainers guide.