mirror of
https://github.com/docker/docs.git
synced 2026-03-28 23:08:49 +07:00
## Description Migrate 17 Testcontainers guides from testcontainers.com into the Docker docs site, covering Java (14 guides), .NET (2 guides), and Node.js (1 guide). This follows up on PR #24450 which added the initial Go and Python guides. Each guide is converted from AsciiDoc to Hugo Markdown, split into multi-chapter stepper navigation, updated to the latest Testcontainers API, and verified with passing tests running in containers. Java guides use testcontainers-java 2.0.4 with the new 2.x Maven coordinates and package names (e.g., `testcontainers-postgresql`, `org.testcontainers.postgresql.PostgreSQLContainer`). The Quarkus guide uses Quarkus 3.22.3 with TC 1.x managed by the Quarkus BOM, since no released Quarkus version ships TC 2.x yet. ## How to test All code snippets have been verified by running each guide's source repository tests inside Docker containers with the Docker socket mounted. To re-run the verification, use the `/testcontainers-guides-migrator` skill included in this PR (`.claude/skills/testcontainers-guides-migrator/SKILL.md`). The skill's Step 6 documents the exact container commands and macOS Docker Desktop workarounds (host override, docker-java API version, etc.) needed to run each language's tests: ``` /testcontainers-guides-migrator I want you to verify all the guides in this branch. Do a full review, verifying that all code snippets compile, the code is executable, and ALL the tests pass. Run them as docker containers, never locally. ``` ## Related issues or tickets Supersedes #24450 (expanded from 2 guides to all 19) ## Reviews - [ ] Technical review - [ ] Editorial review - [ ] Product review --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
496 lines
14 KiB
Markdown
496 lines
14 KiB
Markdown
---
|
|
title: Write tests with WireMock and Testcontainers
|
|
linkTitle: Write tests
|
|
description: Test external REST API integrations using WireMock with both the JUnit 5 extension and the Testcontainers WireMock module.
|
|
weight: 20
|
|
---
|
|
|
|
Mocking external API interactions at the HTTP protocol level, rather than
|
|
mocking Java methods, lets you verify marshalling and unmarshalling behavior and
|
|
simulate network issues.
|
|
|
|
## Test using WireMock JUnit 5 extension
|
|
|
|
WireMock provides a JUnit 5 extension that starts an in-process WireMock server.
|
|
You can configure stub responses using the WireMock Java API.
|
|
|
|
Create `AlbumControllerTest.java`:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
|
|
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
|
import static io.restassured.RestAssured.given;
|
|
import static org.hamcrest.CoreMatchers.is;
|
|
import static org.hamcrest.Matchers.hasSize;
|
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
|
|
|
import com.github.tomakehurst.wiremock.client.WireMock;
|
|
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
|
import io.restassured.RestAssured;
|
|
import io.restassured.http.ContentType;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.test.context.DynamicPropertyRegistry;
|
|
import org.springframework.test.context.DynamicPropertySource;
|
|
|
|
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
|
class AlbumControllerTest {
|
|
|
|
@LocalServerPort
|
|
private Integer port;
|
|
|
|
@RegisterExtension
|
|
static WireMockExtension wireMock = WireMockExtension
|
|
.newInstance()
|
|
.options(wireMockConfig().dynamicPort())
|
|
.build();
|
|
|
|
@DynamicPropertySource
|
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
registry.add("photos.api.base-url", wireMock::baseUrl);
|
|
}
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
RestAssured.port = port;
|
|
}
|
|
|
|
@Test
|
|
void shouldGetAlbumById() {
|
|
Long albumId = 1L;
|
|
|
|
wireMock.stubFor(
|
|
WireMock
|
|
.get(urlMatching("/albums/" + albumId + "/photos"))
|
|
.willReturn(
|
|
aResponse()
|
|
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
|
.withBody(
|
|
"""
|
|
[
|
|
{
|
|
"id": 1,
|
|
"title": "accusamus beatae ad facilis cum similique qui sunt",
|
|
"url": "https://via.placeholder.com/600/92c952",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "reprehenderit est deserunt velit ipsam",
|
|
"url": "https://via.placeholder.com/600/771796",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/771796"
|
|
}
|
|
]
|
|
"""
|
|
)
|
|
)
|
|
);
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(200)
|
|
.body("albumId", is(albumId.intValue()))
|
|
.body("photos", hasSize(2));
|
|
}
|
|
|
|
@Test
|
|
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
|
|
Long albumId = 2L;
|
|
wireMock.stubFor(
|
|
WireMock
|
|
.get(urlMatching("/albums/" + albumId + "/photos"))
|
|
.willReturn(aResponse().withStatus(500))
|
|
);
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(500);
|
|
}
|
|
}
|
|
```
|
|
|
|
Here's what the test does:
|
|
|
|
- `@SpringBootTest` starts the full application on a random port.
|
|
- `@RegisterExtension` creates a `WireMockExtension` that starts WireMock on a
|
|
dynamic port.
|
|
- `@DynamicPropertySource` overrides `photos.api.base-url` to point at the
|
|
WireMock endpoint, so the application talks to WireMock instead of the real
|
|
photo service.
|
|
- `shouldGetAlbumById()` configures a stub response for
|
|
`/albums/{albumId}/photos`, sends a request to the application's
|
|
`/api/albums/{albumId}` endpoint, and verifies the response body.
|
|
- `shouldReturnServerErrorWhenPhotoServiceCallFailed()` configures WireMock to
|
|
return a 500 status and verifies that the application propagates that status to
|
|
the caller.
|
|
|
|
## Stub using JSON mapping files
|
|
|
|
Instead of using the WireMock Java API, you can configure stubs with JSON
|
|
mapping files. Create
|
|
`src/test/resources/wiremock/mappings/get-album-photos.json`:
|
|
|
|
```json
|
|
{
|
|
"mappings": [
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/([0-9]+)/photos"
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
},
|
|
"bodyFileName": "album-photos-resp-200.json"
|
|
}
|
|
},
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/2/photos"
|
|
},
|
|
"response": {
|
|
"status": 500,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/3/photos"
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
},
|
|
"jsonBody": []
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Create the response body file at
|
|
`src/test/resources/wiremock/__files/album-photos-resp-200.json`:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": 1,
|
|
"title": "accusamus beatae ad facilis cum similique qui sunt",
|
|
"url": "https://via.placeholder.com/600/92c952",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "reprehenderit est deserunt velit ipsam",
|
|
"url": "https://via.placeholder.com/600/771796",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/771796"
|
|
}
|
|
]
|
|
```
|
|
|
|
Initialize WireMock to load stubs from the mapping files:
|
|
|
|
```java
|
|
@RegisterExtension
|
|
static WireMockExtension wireMockServer = WireMockExtension
|
|
.newInstance()
|
|
.options(
|
|
wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock")
|
|
)
|
|
.build();
|
|
```
|
|
|
|
With mapping-based stubs in place, create
|
|
`AlbumControllerWireMockMappingTests.java`:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
|
|
import static io.restassured.RestAssured.given;
|
|
import static org.hamcrest.CoreMatchers.is;
|
|
import static org.hamcrest.Matchers.hasSize;
|
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
|
|
|
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
|
|
import io.restassured.RestAssured;
|
|
import io.restassured.http.ContentType;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
|
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;
|
|
|
|
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
|
class AlbumControllerWireMockMappingTests {
|
|
|
|
@LocalServerPort
|
|
private Integer port;
|
|
|
|
@RegisterExtension
|
|
static WireMockExtension wireMockServer = WireMockExtension
|
|
.newInstance()
|
|
.options(
|
|
wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock")
|
|
)
|
|
.build();
|
|
|
|
@DynamicPropertySource
|
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
registry.add("photos.api.base-url", wireMockServer::baseUrl);
|
|
}
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
RestAssured.port = port;
|
|
}
|
|
|
|
@Test
|
|
void shouldGetAlbumById() {
|
|
Long albumId = 1L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(200)
|
|
.body("albumId", is(albumId.intValue()))
|
|
.body("photos", hasSize(2));
|
|
}
|
|
|
|
@Test
|
|
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
|
|
Long albumId = 2L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(500);
|
|
}
|
|
|
|
@Test
|
|
void shouldReturnEmptyPhotos() {
|
|
Long albumId = 3L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(200)
|
|
.body("albumId", is(albumId.intValue()))
|
|
.body("photos", hasSize(0));
|
|
}
|
|
}
|
|
```
|
|
|
|
The tests don't need inline stub definitions because WireMock loads the mappings
|
|
automatically from the classpath.
|
|
|
|
## Test using the Testcontainers WireMock module
|
|
|
|
The [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
|
|
provisions WireMock as a standalone Docker container, based on
|
|
[WireMock Docker](https://github.com/wiremock/wiremock-docker). This approach is
|
|
useful when you want complete isolation between the test JVM and the mock server.
|
|
|
|
Create a mock configuration file at
|
|
`src/test/resources/com/testcontainers/demo/AlbumControllerTestcontainersTests/mocks-config.json`:
|
|
|
|
```json
|
|
{
|
|
"mappings": [
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/([0-9]+)/photos"
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
},
|
|
"bodyFileName": "album-photos-response.json"
|
|
}
|
|
},
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/2/photos"
|
|
},
|
|
"response": {
|
|
"status": 500,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"request": {
|
|
"method": "GET",
|
|
"urlPattern": "/albums/3/photos"
|
|
},
|
|
"response": {
|
|
"status": 200,
|
|
"headers": {
|
|
"Content-Type": "application/json"
|
|
},
|
|
"jsonBody": []
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Create the response body file at
|
|
`src/test/resources/com/testcontainers/demo/AlbumControllerTestcontainersTests/album-photos-response.json`:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"id": 1,
|
|
"title": "accusamus beatae ad facilis cum similique qui sunt",
|
|
"url": "https://via.placeholder.com/600/92c952",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "reprehenderit est deserunt velit ipsam",
|
|
"url": "https://via.placeholder.com/600/771796",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/771796"
|
|
}
|
|
]
|
|
```
|
|
|
|
Create `AlbumControllerTestcontainersTests.java`:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import static io.restassured.RestAssured.given;
|
|
import static org.hamcrest.CoreMatchers.is;
|
|
import static org.hamcrest.Matchers.hasSize;
|
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
|
|
|
import io.restassured.RestAssured;
|
|
import io.restassured.http.ContentType;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
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.junit.jupiter.Container;
|
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
|
import org.wiremock.integrations.testcontainers.WireMockContainer;
|
|
|
|
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
|
@Testcontainers
|
|
class AlbumControllerTestcontainersTests {
|
|
|
|
@LocalServerPort
|
|
private Integer port;
|
|
|
|
@Container
|
|
static WireMockContainer wiremockServer = new WireMockContainer(
|
|
"wiremock/wiremock:3.6.0"
|
|
)
|
|
.withMapping(
|
|
"photos-by-album",
|
|
AlbumControllerTestcontainersTests.class,
|
|
"mocks-config.json"
|
|
)
|
|
.withFileFromResource(
|
|
"album-photos-response.json",
|
|
AlbumControllerTestcontainersTests.class,
|
|
"album-photos-response.json"
|
|
);
|
|
|
|
@DynamicPropertySource
|
|
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
registry.add("photos.api.base-url", wiremockServer::getBaseUrl);
|
|
}
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
RestAssured.port = port;
|
|
}
|
|
|
|
@Test
|
|
void shouldGetAlbumById() {
|
|
Long albumId = 1L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(200)
|
|
.body("albumId", is(albumId.intValue()))
|
|
.body("photos", hasSize(2));
|
|
}
|
|
|
|
@Test
|
|
void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
|
|
Long albumId = 2L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(500);
|
|
}
|
|
|
|
@Test
|
|
void shouldReturnEmptyPhotos() {
|
|
Long albumId = 3L;
|
|
|
|
given()
|
|
.contentType(ContentType.JSON)
|
|
.when()
|
|
.get("/api/albums/{albumId}", albumId)
|
|
.then()
|
|
.statusCode(200)
|
|
.body("albumId", is(albumId.intValue()))
|
|
.body("photos", hasSize(0));
|
|
}
|
|
}
|
|
```
|
|
|
|
Here's what the test does:
|
|
|
|
- The `@Testcontainers` and `@Container` annotations start a
|
|
`WireMockContainer` using the `wiremock/wiremock:3.6.0` Docker image.
|
|
- `withMapping()` loads stub mappings from `mocks-config.json`, and
|
|
`withFileFromResource()` loads the response body file.
|
|
- `@DynamicPropertySource` overrides `photos.api.base-url` to point at the
|
|
WireMock container's base URL.
|
|
- The tests don't contain inline stub definitions because WireMock loads them
|
|
from the JSON configuration files.
|