Skip to Content
Testcontainers ガイドWireMock を使用した REST API 統合のテスト

WireMock を使用した REST API 統合のテスト



コードを入手する

このガイドでは、以下の内容を学びます。

  • 外部の REST API と連携する Spring Boot アプリケーションを作成する方法
  • Testcontainers の WireMock モジュールを使用して外部 API との統合をテストする方法

事前準備

このガイドで達成すること

Spring Boot プロジェクトを作成し、外部の REST API と連携させます。その後、Testcontainers の WireMock モジュールを使用して、外部 REST API との統合をテストします。

はじめに

Spring Initializr を使用して、新しい Spring Boot プロジェクトを作成できます。その際に Spring WebTestcontainers のスターターを選択してください。

アプリケーションを生成したら、RestAssuredWireMock のライブラリをテスト依存関係として追加します

repositories { mavenCentral() } ext { set('wiremockTestcontainersVersion', "1.0-alpha-13") } dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.wiremock:wiremock-standalone:3.6.0' testImplementation "org.wiremock.integrations.testcontainers:wiremock-testcontainers-module:${wiremockTestcontainersVersion}" }

今回はビデオアルバムを管理するアプリケーションを作成し、画像や動画のアセットを管理するためにサードパーティの REST API を使用します。このガイドでは、公開されている REST API https://jsonplaceholder.typicode.com/photo-service として使用し、アルバムの写真を保存する想定で進めます。

このアプリケーションでは、指定された albumId に対応するアルバムを取得する REST API エンドポイントを実装します。この API は内部的に photo-service にリクエストを送り、そのアルバムに含まれる写真を取得します。

外部サービスとの通信をテストするために、WireMock を使用します。WireMock は モック API を構築するためのツール であり、外部サービスの挙動を模倣できます。

また、Testcontainers の WireMock モジュール を使用することで、WireMock を Docker コンテナ として実行できるため、より柔軟で一貫性のあるテスト環境を構築できます。

Album と Photo のモデルを作成する

まず、Java の record を使用して、AlbumPhoto のモデルを作成します。

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) {}

PhotoServiceClient の作成

次に、RestTemplate を使用して指定された albumId に対応する写真を取得する PhotoServiceClient を作成します。

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(); } }

外部 API(フォトサービス)のベース URL を 設定ファイルで管理 するために、以下のプロパティを src/main/resources/application.properties に追加します。

photos.api.base-url=https://jsonplaceholder.typicode.com

albumId に基づいてアルバムを取得する API エンドポイントの実装

次に、指定された albumId に対応するアルバムを取得する REST API エンドポイントを実装します。

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()); } } }

私たちのアプリケーションは、GET /api/albums/{albumId} REST API エンドポイントを公開しています。

このエンドポイントは、指定された albumId に対応するアルバム情報を取得し、外部 API https://jsonplaceholder.typicode.com/albums/{albumId}/photos にリクエストを送り、そのアルバムの写真を取得します。

{ "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" }, ... ... ] }

アプリケーションを起動し、http://localhost:8080/api/albums/1 にアクセスすることで、JSON レスポンスを確認できます。

次に、WireMock を使用して photo-service との統合をテストする方法を説明します。

photo-service API 統合テストの作成

外部 API とのやり取りを photoServiceClient.getPhotos(albumId) メソッドのモックではなく HTTP プロトコルレベルでモック する方が望ましいです。その理由として、以下の点が挙げられます。

  • シリアライズ / デシリアライズ(Marshaling / Unmarshalling)エラーの検証が可能

  • ネットワーク遅延(Latency)などのシミュレーションが可能

  • 実際の HTTP リクエストとレスポンスのやり取りを検証できる

WireMockExtension を使用して WireMock サーバーのインスタンス を作成し、API 呼び出しに対する期待値を設定することができます。次のように実装します。

@RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); wireMock.stubFor( WireMock.get(urlMatching("/albums/*/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" } ] """)) );

では、GET /api/albums/{albumId} API エンドポイントのテストを次のように作成しましょう。

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); } }

このテストの内容を理解しましょう

  • このテストクラスには @SpringBootTest アノテーションを付与しており、Spring アプリケーションのコンテキスト全体をロードする統合テスト になっています。

  • RestAssured ライブラリを使用して API エンドポイントのテストを行います。アプリケーションが起動した際のランダムなポートを取得し、それを RestAssured のポートとして設定しています。

  • photos.api.base-url プロパティを WireMock のエンドポイント URL に登録します

  • shouldGetAlbumById() テストでは、/albums/{albumId}/photos API 呼び出しの期待されるモックレスポンスを設定し、アプリケーションの /api/albums/{albumId} エンドポイントにリクエストを送信し、レスポンスを検証しています。

  • shouldReturnServerErrorWhenPhotoServiceCallFailed() テストでは、/albums/{albumId}/photos API 呼び出しの期待されるモックレスポンスを 500 Internal Server Error ステータスコードを返すように設定し、アプリケーションの /api/albums/{albumId} エンドポイントにリクエストを送信し、レスポンスを検証しています。

JSON マッピングファイルを使用したスタブの設定

前回のテストでは、wireMock.stubFor(…​) を使用して API をスタブする方法を確認しました。WireMock の Java API を使用する代わりに、JSON マッピングを使用した設定 を行うこともできます。

次のように、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" }, "jsonBody": [ { "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" }, { ... } ] } } ] }

次のようにして、マッピングファイルからスタブマッピングを読み込むことで WireMock を初期化できます。

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

マッピングファイルを使用したスタブ設定を行った状態で、次のようにテストを記述できます。

@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)); }

Testcontainers WireMock モジュールの使用

Testcontainers の WireMock モジュールを使用すると、WireMock サーバーを Docker ベースのスタンドアロンコンテナとしてテスト内でプロビジョニング できます。

次のように AlbumControllerTestcontainersTests を作成し、WireMockContainer を使用して WireMock サーバーを初期化し、スタブを設定します。

@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"); @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(1)); } }
  • @Testcontainers および @Container アノテーションを使用

    • Testcontainers の JUnit 5 拡張機能を利用し、WireMockContainer を初期化しています。
  • mocks-config.json ファイルからスタブマッピングを読み込むように設定

次のように、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" }, "jsonBody": [ { "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" } ] } } ] }

これでテストを実行すると、photo-service API への呼び出しは、mocks-config.json ファイルで定義された WireMock のスタブを使用してレスポンスを受け取るようになります。

テストの実行

# Maven を使用している場合 ./mvnw test # Gradle を使用している場合 ./gradlew test

これでテストを実行すると、コンソールログに WireMock の Docker インスタンスが起動したことが表示されます。このインスタンスは photo-service の代わりとして動作し、設定された期待値に従ってモックレスポンスを提供 します。

テストは正常に実行され、成功するはずです。

まとめ

Spring Boot アプリケーションでサードパーティの HTTP API を統合し、Testcontainers WireMock モジュールを使用してテストする方法を学びました。

💡

Testcontainers WireMock モジュールは GoPython の言語向けにも提供されています。

Testcontainers WireMock モジュールの詳細な使用方法については、公式ドキュメント https://wiremock.org/docs/solutions/testcontainers/ を参照してください。

Testcontainers についてさらに詳しく知りたい場合は、https://testcontainers.com をご覧ください。

参考資料

Last updated on