外部REST APIを呼び出すServiceクラスのテスト、ちゃんと書けていますか?

MockitoでHTTPクライアントをモックすれば一応テストはできますが、「実際のHTTP通信は走っていない」という問題があります。タイムアウトの挙動や5xxエラー時の動作を確認したい場合、Mockitoだけでは対応できないんですよね。

そこで使いたいのが WireMock です。ローカルにHTTPサーバーを立てて、外部APIをHTTPレベルでスタブ化できます。この記事では導入から実践的なスタブ定義まで一通り解説します。

MockitoとWireMockの役割の違い

まず整理しておきましょう。

Mockito はクラス境界でモックを差し込みます。RestTemplateのBeanをモックすれば exchange() の戻り値を制御できますが、実際のHTTPソケット通信は発生しません。接続タイムアウトやSSLエラーなどのネットワーク層の挙動は再現できません。

WireMock はローカルにHTTPサーバーを起動し、指定したパスへのリクエストに対して任意のレスポンスを返します。RestTemplateもWebClientも「本物のHTTPリクエスト」を投げるので、HTTP設定の誤りやタイムアウト動作も含めて検証できます。

判断軸はシンプルで、「HTTP層を含めてテストしたいか」どうかです。JUnit/Mockitoを使った単体テストで十分な場合はMockitoを、RestTemplate/WebClientの実通信を含む場合はWireMockを選びましょう。

依存関係の追加

wiremock-spring-boot はSpring Boot向けのオートコンフィグが付属しており、セットアップが最小限で済みます。

Maven の場合

<dependency>
    <groupId>org.wiremock.integrations</groupId>
    <artifactId>wiremock-spring-boot</artifactId>
    <!-- 最新バージョンはMaven Central(org.wiremock.integrations:wiremock-spring-boot)で確認してください -->
    <version>3.1.0</version>
    <scope>test</scope>
</dependency>

Gradle の場合

// 最新バージョンはwiremock-spring-boot GitHubのReleasesページで確認してください
testImplementation 'org.wiremock.integrations:wiremock-spring-boot:3.1.0'

なお、com.github.tomakehurst:wiremock-jre8 はWireMock 2.x時代のアーティファクトなので、新規プロジェクトでは使わないようにしましょう。

@SpringBootTest + @EnableWireMockを使ったセットアップ

wiremock-spring-boot を使う場合、@SpringBootTest@EnableWireMock を組み合わせるのがメインの構成です。Springコンテキストが起動するので @Autowired でServiceを注入できます。

importについて: WireMock 3.xではパッケージが org.wiremock に移行しています。wiremock-spring-boot 3.x 環境では org.wiremock.client.WireMock.* が推奨インポートです。旧パッケージ com.github.tomakehurst.wiremock.client.WireMock.* は後方互換性のために残っていますが、公式ドキュメントと照らし合わせるときに混乱しないよう新パッケージを使いましょう。

また、@EnableWireMock を使うとテストメソッドごとにスタブ定義とリクエスト履歴が自動リセットされます。前のテストのスタブが次のテストに残ることはありません。

import static org.wiremock.client.WireMock.*;

@SpringBootTest
@EnableWireMock
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void ユーザー取得_正常系() {
        stubFor(get(urlEqualTo("/api/users/1"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"id\": 1, \"name\": \"Taro\"}")));

        User user = userService.findById(1L);
        assertThat(user.getName()).isEqualTo("Taro");
    }
}

コアWireMockライブラリが提供する @WireMockTest を使う方法もありますが、こちらはSpringコンテキストを起動しません。@Autowired は機能せず、Serviceは new で手動インスタンス化する必要があります。Spring Bootのコンテキストが必要なテストは @SpringBootTest + @EnableWireMock を使いましょう。

RestTemplate/WebClientのベースURLをWireMockに向ける

@EnableWireMock を使うと wiremock.server.port プロパティが自動登録されます(@WireMockTest 単独では登録されません)。application-test.properties にこう書くだけです。

api.base-url=http://localhost:${wiremock.server.port}

Serviceクラス側では @Value("${api.base-url}") でURLを受け取るように実装しておきましょう。RestTemplate/WebClientの設定方法はこちらの記事も参考にしてください。

スタブ定義パターン:stubForの基本

レスポンスボディにJSONファイルを使いたい場合は withBodyFile が便利です。src/test/resources/__files/ 配下にファイルを置く慣例になっています。

// src/test/resources/__files/responses/user.json にJSONを配置
stubFor(get(urlEqualTo("/api/users/1"))
    .willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "application/json")
        .withBodyFile("responses/user.json")));

POSTリクエストのスタブも同じように書けます。

stubFor(post(urlEqualTo("/api/users"))
    .willReturn(aResponse()
        .withStatus(201)
        .withHeader("Content-Type", "application/json")
        .withBody("{\"id\": 2, \"name\": \"Jiro\"}")));

エラーレスポンス(4xx/5xx)の再現

実APIで意図的に500を返させるのは難しいですよね。WireMockなら簡単です。

@Test
void ユーザー取得_サーバーエラー時は例外をスロー() {
    stubFor(get(urlEqualTo("/api/users/1"))
        .willReturn(aResponse().withStatus(500)));

    assertThrows(HttpServerErrorException.class, () -> {
        userService.findById(1L);
    });
}

RestTemplateは4xxで HttpClientErrorException、5xxで HttpServerErrorException をスローします。WebClientを使っている場合は onStatus で処理している WebClientResponseException を検証しましょう。

タイムアウトのシミュレーション(withFixedDelay)

withFixedDelay でレスポンスを意図的に遅延させることができます。ReadTimeoutを短く設定したRestTemplate Beanをテスト用に用意し、@Import で組み込みます。

import static org.wiremock.client.WireMock.*;

@SpringBootTest
@EnableWireMock
@Import(TimeoutTest.TimeoutConfig.class)
class TimeoutTest {

    @Autowired
    private UserService userService;

    @Test
    void ユーザー取得_タイムアウト時はResourceAccessExceptionをスロー() {
        stubFor(get(urlEqualTo("/api/users/1"))
            .willReturn(aResponse()
                .withStatus(200)
                .withFixedDelay(3000))); // 3秒遅延

        assertThrows(ResourceAccessException.class, () -> {
            userService.findById(1L);
        });
    }

    @TestConfiguration
    static class TimeoutConfig {
        @Bean
        @Primary
        public RestTemplate restTemplate(RestTemplateBuilder builder) {
            return builder
                .setReadTimeout(Duration.ofSeconds(1))
                .build();
        }
    }
}

タイムアウト後にリトライやフォールバックが動くかの検証にも使えます。Resilience4jのサーキットブレーカーを組み合わせているサービスなら、WireMockで遅延を再現してサーキットが開くかどうかまでテストできます。

リクエスト検証(verify)でAPI呼び出しを確認する

スタブでレスポンスを返すだけでなく、「正しいリクエストを送れているか」を検証することも大事です。

@Test
void ユーザー取得_正しいエンドポイントを呼び出す() {
    stubFor(get(urlEqualTo("/api/users/1"))
        .willReturn(aResponse().withStatus(200).withBody("{\"id\": 1}")));

    userService.findById(1L);

    verify(1, getRequestedFor(urlEqualTo("/api/users/1"))
        .withHeader("Accept", equalTo("application/json")));
}

POSTリクエストのボディ検証は withRequestBody(containing("...")) で確認できます。呼び出し回数が気になる場合は verify(exactly(1), ...)verify(moreThan(0), ...) を使いましょう。

MockitoとWireMockの使い分けまとめ

状況選ぶべきツール
HTTPを介さない内部依存のモックMockito
RestTemplate/WebClientの実HTTP通信を含むWireMock
タイムアウトやネットワークエラーの再現WireMock
DBの統合テストTestcontainers

Testcontainersを使った統合テストと組み合わせれば、DBはTestcontainers・外部HTTPはWireMockという構成でカバレッジを高めることができます。

まとめ

WireMockを使うとHTTPレベルでの外部APIスタブが手軽に書けます。正常系だけでなくエラー系・タイムアウトまでテストできるので、実APIに依存した不安定なテストから解放されますよ。verify でリクエストの中身まで確認できるようになれば、外部API連携のテストが格段に充実します。