--- title: Write tests with WireMock and Testcontainers linkTitle: Write tests description: Test external REST API integrations using WireMock 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 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`: ```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 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: - `WireMockExtension` starts a WireMock server on a dynamic port. - The `getProperties()` method overrides `micronaut.http.services.photosapi.url` to 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`: ```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`: ```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: ```java @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: ```java @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](https://testcontainers.com/modules/wiremock/) provisions a WireMock server as a standalone container within your tests, based on [WireMock Docker](https://github.com/wiremock/wiremock-docker). Create `src/test/resources/mocks-config.json` with the stub mappings: ```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 `src/test/resources/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.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 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: - `@Testcontainers` and `@Container` annotations start a `WireMockContainer` using the `wiremock/wiremock:2.35.0` Docker 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.