Files
docker-docs/content/guides/testcontainers-java-wiremock/write-tests.md
Manuel de la Peña b951e92f57 feat(guides): migrate all testcontainers.com guides (#24505)
## 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>
2026-03-25 10:03:26 +00:00

14 KiB

title, linkTitle, description, weight
title linkTitle description weight
Write tests with WireMock and Testcontainers Write tests Test external REST API integrations using WireMock with both the JUnit 5 extension and the Testcontainers WireMock module. 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:

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:

{
  "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:

[
  {
    "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:

@RegisterExtension
static WireMockExtension wireMockServer = WireMockExtension
  .newInstance()
  .options(
    wireMockConfig().dynamicPort().usingFilesUnderClasspath("wiremock")
  )
  .build();

With mapping-based stubs in place, create AlbumControllerWireMockMappingTests.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 provisions WireMock as a standalone Docker container, based on 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:

{
  "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:

[
  {
    "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.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.