外部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層を含めてテストしたいか」どうかです。Serviceの内部依存だけを差し替えたいならMockitoを、RestTemplate/WebClientの実通信を含めたいならWireMockを選びましょう。JUnit 5とMockitoの基本は JUnit/Mockitoを使った単体テスト で、when / verify / ArgumentCaptor を使ったServiceクラスのテストの書き方は MockitoでServiceクラスをテストする方法 で解説しています。

Spring BootでWireMockを使う準備(wiremock-spring-bootの依存関係を追加)

Spring BootでWireMockを使うなら、WireMock公式が提供するSpring Boot連携ライブラリ wiremock-spring-boot を入れるのが一番の近道です。@EnableWireMock を付けるだけでテスト起動時にWireMockサーバーが立ち上がり、ポートやベースURLがSpringのプロパティとして自動登録されます。WireMock本体を手動で起動したり、@DynamicPropertySource でURLを差し込んだりするコードは不要になります。

依存関係は test スコープで1行追加するだけです。

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'

wiremock-spring-boot はWireMock本体を推移的に引き込むので、WireMock自体を別途追加する必要はありません。なお、com.github.tomakehurst:wiremock-jre8 はWireMock 2.x時代のアーティファクトなので、新規プロジェクトでは使わないようにしましょう。

org.wiremock.client.WireMockの正しいimport文(WireMock 3.x対応)

WireMock 3.x(および wiremock-spring-boot 3.x)では、stubFor / get / urlEqualTo などの静的メソッドが配置されているパッケージが変更されています。検索でこのページに辿り着いた方はまずここを確認してください。

WireMock 3.x(推奨・新パッケージ)

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

個別importの場合はこうなります。

import static org.wiremock.client.WireMock.stubFor;
import static org.wiremock.client.WireMock.get;
import static org.wiremock.client.WireMock.post;
import static org.wiremock.client.WireMock.urlEqualTo;
import static org.wiremock.client.WireMock.aResponse;
import static org.wiremock.client.WireMock.verify;
import static org.wiremock.client.WireMock.getRequestedFor;
import static org.wiremock.client.WireMock.equalTo;

WireMock 2.x / 旧パッケージ(後方互換のため残存)

import static com.github.tomakehurst.wiremock.client.WireMock.*;

新旧パッケージの違いと選び方

パッケージ対応バージョン用途
org.wiremock.client.WireMockWireMock 3.x新規プロジェクトはこちら
com.github.tomakehurst.wiremock.client.WireMockWireMock 2.x〜(3.xでも後方互換で利用可)既存コードの移行猶予用

com.github.tomakehurst:wiremock-jre8 のような旧アーティファクトを pom.xml / build.gradle に書いていると、IDEで org.wiremock.client.WireMock を補完しても解決できません。依存関係が org.wiremock.integrations:wiremock-spring-boot (または org.wiremock:wiremock-standalone)になっているかを先に確認しましょう。

IDEで org.wiremock.client.WireMock が解決できないときのチェックリスト

  • 依存関係が org.wiremock.integrations:wiremock-spring-boot:3.x になっているか
  • wiremock-jre8 など 2.x 系の依存が残っていないか(混在するとimport補完がブレます)
  • Maven/Gradleのキャッシュ更新(mvn -U / ./gradlew --refresh-dependencies)を実行したか
  • テストクラスのスコープが test 配下にあるか

ここまで確認できたら、次はテストクラスのセットアップに進みましょう。

@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/RestClientのベースURLをWireMockに向ける

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

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

Serviceクラス側では @Value("${api.base-url}") でURLを受け取るように実装しておきましょう。

@ConfigureWireMockのbaseUrlPropertiesでプロパティファイルを省略する

wiremock-spring-boot 3.xなら、@ConfigureWireMock の baseUrlProperties にアプリ側のプロパティ名を直接指定できます。テスト用のpropertiesファイルを用意しなくても、api.base-url にWireMockのURLが注入されます。

@SpringBootTest
@EnableWireMock({
    @ConfigureWireMock(
        name = "user-api",
        baseUrlProperties = "api.base-url"
    )
})
class UserServiceTest {
    // スタブ定義は後述の stubFor(...) と同じ書き方です
}

連携先が複数ある場合は @ConfigureWireMock を並べて、それぞれ別の name と baseUrlProperties を割り当てましょう。外部API 1つにつき1台のWireMockサーバーが立つので、スタブが混ざりません。

RestClientを使っている場合

Spring Boot 3.2以降で標準になった RestClient でも、やることは同じです。RestClient.Builder に注入したベースURLを渡すだけで、スタブ定義側のコードは一切変わりません。

@Service
public class UserService {

    private final RestClient restClient;

    public UserService(RestClient.Builder builder,
                       @Value("${api.base-url}") String baseUrl) {
        this.restClient = builder.baseUrl(baseUrl).build();
    }

    public User findById(Long id) {
        return restClient.get()
            .uri("/api/users/{id}", id)
            .retrieve()
            .body(User.class);
    }
}

RestClientはデフォルトで4xxなら HttpClientErrorException、5xxなら HttpServerErrorException をスローするので、後述のエラー再現テストもRestTemplateと同じ書き方で通ります。各クライアントの設定の詳細は RestTemplate/WebClientの設定方法 と RestClientの使い方 で解説しています。

スタブ定義パターン: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/RestClientの実HTTP通信を含むWireMock
タイムアウトやネットワークエラーの再現WireMock
DBの統合テストTestcontainers

DBはTestcontainers、外部HTTPはWireMockという構成にすれば、本番に近い形で統合テストのカバレッジを高められます。Testcontainersの導入手順は Testcontainersを使った統合テスト で解説しています。@SpringBootTest の webEnvironment をMOCKとRANDOM_PORTのどちらにするか迷ったら、@SpringBootTestのwebEnvironment設定の選び方 も参考にしてください。

まとめ

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