Files
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

12 KiB

title, linkTitle, description, weight
title linkTitle description weight
Write tests with WireMock and Testcontainers Write tests Test external REST API integrations using WireMock 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 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:

  • 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:

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

  • @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.