mirror of
https://github.com/docker/docs.git
synced 2026-10-25 06:09:46 +03:00
## Description Follow-up to #25338 (the guides revamp). That PR collapsed most multi-page guide series into single pages but left ~50 guides wrapped in a `<name>/index.md` directory structure alongside guides that are already flat `<name>.md` files — an inconsistent mix. This PR finishes the flattening: - Flattens 50 folder-wrapped guides to `content/guides/<slug>.md` via `git mv` (history preserved). These keep the same URL, since Hugo generates the same permalink for a leaf-bundle `dir/index.md` and a flat `dir.md`. - Moves the last 4 guides' local per-guide image bundles (`genai-leveraging-rag`, `genai-video-bot`, `genai-claude-code-mcp`, `java`) into the shared `content/guides/images/` folder with slug-prefixed filenames, matching the convention already used by most other guides (`databases-1.webp`, `kafka-1.webp`, etc.), and rewrites the in-body image references. - Normalizes `genai-claude-code-mcp`'s non-standard filename (`claude-code-mcp-guide.md`) and capitalized `Images/` folder to match convention. - Promotes `content/guides/frameworks/laravel/index.md` to a top-level `content/guides/laravel.md`, since it was the only guide under `frameworks/` and every other language/framework guide is flat. - Adds `aliases` entries for the two URLs that actually changed (`/guides/frameworks/laravel/` and `/guides/genai-claude-code-mcp/claude-code-mcp-guide/`); every other rename preserves its existing URL. Companion PR establishing this as the documented convention going forward: #25503 ## Validation - `hugo --minify` build via `docker compose run` completes with zero warnings/errors (3174 pages, 1524 aliases — unchanged from before this PR). - Verified both changed-URL guides render at their new path and the old path 301s via the Hugo alias redirect. - Verified every image reference in the 4 migrated guides resolves to an existing file in `content/guides/images/`. - Multi-agent docs review (link-checker, cross-reference-checker, guide-conventions) found zero issues. ## Related issues or tickets Discussed in the team's docs Slack channel following #25338. ## Reviews - [ ] Technical review
771 lines
22 KiB
Markdown
771 lines
22 KiB
Markdown
---
|
|
title: Testing REST API integrations using WireMock
|
|
linkTitle: WireMock
|
|
description: Learn how to test REST API integrations in a Spring Boot application using the Testcontainers WireMock module.
|
|
keywords: testcontainers, java, spring boot, testing, wiremock, rest api, rest assured
|
|
summary: |
|
|
Learn how to create a Spring Boot application that integrates with
|
|
external REST APIs, then test those integrations using Testcontainers
|
|
and WireMock.
|
|
aliases:
|
|
- /guides/testcontainers-java-wiremock/create-project/
|
|
- /guides/testcontainers-java-wiremock/run-tests/
|
|
- /guides/testcontainers-java-wiremock/write-tests/
|
|
params:
|
|
tags: [testing]
|
|
time: 20 minutes
|
|
---
|
|
|
|
|
|
<!-- Source: https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-wiremock -->
|
|
|
|
In this guide, you'll learn how to:
|
|
|
|
- Create a Spring Boot application that talks to external REST APIs
|
|
- Test external API integrations using WireMock with both the JUnit 5 extension
|
|
and the Testcontainers WireMock module
|
|
|
|
## Prerequisites
|
|
|
|
- Java 17+
|
|
- Maven or Gradle
|
|
- A Docker environment supported by Testcontainers
|
|
|
|
> [!NOTE]
|
|
> If you're new to Testcontainers, visit the
|
|
> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about
|
|
> Testcontainers and the benefits of using it.
|
|
|
|
## Create the Spring Boot project
|
|
|
|
### Set up the project
|
|
|
|
Create a Spring Boot project from [Spring Initializr](https://start.spring.io)
|
|
by selecting the **Spring Web** and **Testcontainers** starters.
|
|
|
|
Alternatively, clone the
|
|
[guide repository](https://github.com/testcontainers/tc-guide-testing-rest-api-integrations-using-wiremock).
|
|
|
|
After generating the project, add the **REST Assured**, **WireMock**, and
|
|
**WireMock Testcontainers module** libraries as test dependencies. The key
|
|
dependencies in `pom.xml` are:
|
|
|
|
```xml
|
|
<properties>
|
|
<java.version>17</java.version>
|
|
<testcontainers.version>2.0.4</testcontainers.version>
|
|
<wiremock-testcontainers.version>1.0-alpha-13</wiremock-testcontainers.version>
|
|
</properties>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-web</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.testcontainers</groupId>
|
|
<artifactId>testcontainers-junit-jupiter</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.wiremock</groupId>
|
|
<artifactId>wiremock-standalone</artifactId>
|
|
<version>3.6.0</version>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.wiremock.integrations.testcontainers</groupId>
|
|
<artifactId>wiremock-testcontainers-module</artifactId>
|
|
<version>${wiremock-testcontainers.version}</version>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>io.rest-assured</groupId>
|
|
<artifactId>rest-assured</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
```
|
|
|
|
Using the Testcontainers BOM (Bill of Materials) is recommended so that you
|
|
don't have to repeat the version for every Testcontainers module dependency.
|
|
|
|
This guide builds an application that manages video albums. A third-party REST
|
|
API handles photo assets. For demonstration purposes, the application uses the
|
|
publicly available [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API
|
|
as a photo service.
|
|
|
|
The application exposes a `GET /api/albums/{albumId}` endpoint that calls the
|
|
photo service to fetch photos for a given album.
|
|
[WireMock](https://wiremock.org/) is a tool for building mock APIs.
|
|
Testcontainers provides a
|
|
[WireMock module](https://testcontainers.com/modules/wiremock/) that runs
|
|
WireMock as a Docker container.
|
|
|
|
### Create the Album and Photo models
|
|
|
|
Create `Album.java` using Java records:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import java.util.List;
|
|
|
|
public record Album(Long albumId, List<Photo> photos) {}
|
|
|
|
record Photo(Long id, String title, String url, String thumbnailUrl) {}
|
|
```
|
|
|
|
### Create the PhotoServiceClient
|
|
|
|
Create `PhotoServiceClient.java`, which uses `RestTemplate` to fetch photos for
|
|
a given album ID:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import java.util.List;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
import org.springframework.core.ParameterizedTypeReference;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
@Service
|
|
class PhotoServiceClient {
|
|
|
|
private final String baseUrl;
|
|
private final RestTemplate restTemplate;
|
|
|
|
PhotoServiceClient(
|
|
@Value("${photos.api.base-url}") String baseUrl,
|
|
RestTemplateBuilder builder
|
|
) {
|
|
this.baseUrl = baseUrl;
|
|
this.restTemplate = builder.build();
|
|
}
|
|
|
|
List<Photo> getPhotos(Long albumId) {
|
|
String url = baseUrl + "/albums/{albumId}/photos";
|
|
ResponseEntity<List<Photo>> response = restTemplate.exchange(
|
|
url,
|
|
HttpMethod.GET,
|
|
null,
|
|
new ParameterizedTypeReference<>() {},
|
|
albumId
|
|
);
|
|
return response.getBody();
|
|
}
|
|
}
|
|
```
|
|
|
|
The photo service base URL is externalized as a configuration property. Add the
|
|
following entry to `src/main/resources/application.properties`:
|
|
|
|
```properties
|
|
photos.api.base-url=https://jsonplaceholder.typicode.com
|
|
```
|
|
|
|
### Create the REST API endpoint
|
|
|
|
Create `AlbumController.java`:
|
|
|
|
```java
|
|
package com.testcontainers.demo;
|
|
|
|
import java.util.List;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import org.springframework.web.client.RestClientResponseException;
|
|
|
|
@RestController
|
|
@RequestMapping("/api")
|
|
class AlbumController {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(
|
|
AlbumController.class
|
|
);
|
|
|
|
private final PhotoServiceClient photoServiceClient;
|
|
|
|
AlbumController(PhotoServiceClient photoServiceClient) {
|
|
this.photoServiceClient = photoServiceClient;
|
|
}
|
|
|
|
@GetMapping("/albums/{albumId}")
|
|
public ResponseEntity<Album> getAlbumById(@PathVariable Long albumId) {
|
|
try {
|
|
List<Photo> photos = photoServiceClient.getPhotos(albumId);
|
|
return ResponseEntity.ok(new Album(albumId, photos));
|
|
} catch (RestClientResponseException e) {
|
|
logger.error("Failed to get photos", e);
|
|
return new ResponseEntity<>(e.getStatusCode());
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
This endpoint calls the photo service for a given album ID and returns a
|
|
response like:
|
|
|
|
```json
|
|
{
|
|
"albumId": 1,
|
|
"photos": [
|
|
{
|
|
"id": 51,
|
|
"title": "non sunt voluptatem placeat consequuntur rem incidunt",
|
|
"url": "https://via.placeholder.com/600/8e973b",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/8e973b"
|
|
},
|
|
{
|
|
"id": 52,
|
|
"title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
|
|
"url": "https://via.placeholder.com/600/121fa4",
|
|
"thumbnailUrl": "https://via.placeholder.com/150/121fa4"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Write tests with WireMock and Testcontainers
|
|
|
|
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.
|
|
|
|
## Run tests and next steps
|
|
|
|
### Run the tests
|
|
|
|
```console
|
|
$ ./mvnw test
|
|
```
|
|
|
|
Or with Gradle:
|
|
|
|
```console
|
|
$ ./gradlew test
|
|
```
|
|
|
|
You should see the WireMock Docker container start in the console output. It
|
|
acts as the photo service, serving mock responses based on the configured
|
|
expectations. All tests should pass.
|
|
|
|
### Summary
|
|
|
|
You built a Spring Boot application that integrates with an external REST API,
|
|
then tested that integration using three different approaches:
|
|
|
|
- WireMock JUnit 5 extension with inline stubs
|
|
- WireMock JUnit 5 extension with JSON mapping files
|
|
- Testcontainers WireMock module running WireMock in a Docker container
|
|
|
|
Testing at the HTTP protocol level instead of mocking Java methods lets you
|
|
catch serialization issues and simulate realistic failure scenarios.
|
|
|
|
To learn more about Testcontainers, visit the
|
|
[Testcontainers overview](https://testcontainers.com/getting-started/).
|
|
|
|
### Further reading
|
|
|
|
- [Testcontainers WireMock module](https://testcontainers.com/modules/wiremock/)
|
|
- [WireMock documentation](https://wiremock.org/docs/)
|
|
- [Testcontainers JUnit 5 quickstart](https://java.testcontainers.org/quickstart/junit_5_quickstart/)
|