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 with WireMock's JUnit 5 extension
The first approach uses WireMock's WireMockExtension to start an in-process
WireMock server on a dynamic port.
Create AlbumControllerTest.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 com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.MediaType;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class AlbumControllerTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
private Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url", wireMock.baseUrl());
}
@Test
void shouldGetAlbumById() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
Long albumId = 1L;
String responseJson =
"""
[
{
"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"
}
]
""";
wireMock.stubFor(WireMock.get(urlMatching("/albums/" + albumId + "/photos"))
.willReturn(aResponse()
.withHeader("Content-Type", MediaType.APPLICATION_JSON)
.withBody(responseJson)));
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() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
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 this test does:
WireMockExtensionstarts a WireMock server on a dynamic port.- The
getProperties()method overridesmicronaut.http.services.photosapi.urlto point at the WireMock endpoint, so the application talks to WireMock instead of the real photo service. shouldGetAlbumById()configures a mock 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 the application propagates that error.
Stub using JSON mapping files
Instead of stubbing with the WireMock Java API, you can use JSON mapping-based configuration.
Create src/test/resources/wiremock/mappings/get-album-photos.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 src/test/resources/wiremock/__files/album-photos-resp-200.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"
}
]Then initialize WireMock to load stub mappings from these files:
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(
wireMockConfig()
.dynamicPort()
.usingFilesUnderClasspath("wiremock")
)
.build();With mapping files-based stubbing in place, write tests without needing programmatic stubs:
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", hasSize(2));
}
}Use the Testcontainers WireMock module
The Testcontainers WireMock module provisions a WireMock server as a standalone container within your tests, based on WireMock Docker.
Create src/test/resources/mocks-config.json with the stub mappings:
{
"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 src/test/resources/album-photos-response.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:
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.hamcrest.Matchers.nullValue;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.runtime.server.EmbeddedServer;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.wiremock.integrations.testcontainers.WireMockContainer;
@Testcontainers(disabledWithoutDocker = true)
class AlbumControllerTestcontainersTests {
@Container
static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:2.35.0")
.withMappingFromResource("mocks-config.json")
.withFileFromResource("album-photos-response.json");
@NonNull public Map<String, Object> getProperties() {
return Collections.singletonMap("micronaut.http.services.photosapi.url", wiremockServer.getBaseUrl());
}
@Test
void shouldGetAlbumById() {
Long albumId = 1L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
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;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(500);
}
}
@Test
void shouldReturnEmptyPhotos() {
Long albumId = 3L;
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class, getProperties())) {
RestAssured.port = server.getPort();
given().contentType(ContentType.JSON)
.when()
.get("/api/albums/{albumId}", albumId)
.then()
.statusCode(200)
.body("albumId", is(albumId.intValue()))
.body("photos", nullValue());
}
}
}Here's what this test does:
@Testcontainersand@Containerannotations start aWireMockContainerusing thewiremock/wiremock:2.35.0Docker image.withMappingFromResource("mocks-config.json")loads stub mappings from the classpath resource.withFileFromResource("album-photos-response.json")makes the response body file available to WireMock.getProperties()overrides the photo service URL to point at the WireMock container's base URL.shouldGetAlbumById()verifies that the application returns the expected album with two photos.shouldReturnServerErrorWhenPhotoServiceCallFailed()verifies that a 500 from the photo service propagates to the caller.shouldReturnEmptyPhotos()verifies the application handles an empty photo list.