JUnitとMockitoの基礎が身についてくると、@SpringBootTestはとりあえずRANDOM_PORTで全部書いてしまいがちですよね。でもwebEnvironmentを意識しないと、不要に重いテストばかり積み重なります。

この記事ではwebEnvironmentの4モードが何を起動し何を起動しないのかを比較表と動くコードで整理し、テスト対象に合わせて最も軽いモードを選べるようにします。@SpringBootTest自体の基本は上記の記事で扱っているので、ここではwebEnvironmentだけに絞ります。

webEnvironmentのデフォルトはMOCK

webEnvironmentは@SpringBootTestがApplicationContextをどう起動するかを決めるオプションで、指定しなければデフォルトはMOCKです。「RANDOM_PORTにしておけば安全」という思い込みでモードを変えると、実サーバー起動のコストを毎回払うことになります。

@SpringBootTest // webEnvironment = WebEnvironment.MOCK と同じ
class OrderServiceDefaultTest {
}

4つのモードの比較表

モード実サーバー起動使えるクライアント典型的な用途コスト
MOCKしないMockMvcController層の単体検証低
RANDOM_PORTする(空きポート)TestRestTemplate / WebTestClient実HTTPを通したE2E検証高
DEFINED_PORTする(固定ポート)TestRestTemplate / WebTestClient固定ポート前提の外部連携検証高
NONEしないなしWebに依存しないサービス層の検証最低

「Webを触らないならNONE、Controllerだけ見るならMOCK」という方針がこれだけで見えてきます。

MOCKでController層を検証する

実サーバーを起動せずMockMvc経由で検証します。実ポートが開かないので@LocalServerPortは使えません。

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerMockTest {
    @Autowired MockMvc mockMvc;

    @Test
    void returnsOrderDetail() throws Exception {
        mockMvc.perform(get("/api/orders/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.status").value("PAID"));
    }
}

業務ロジックの検証は大半がここで足ります。MockMvc自体の詳細は@WebMvcTestでのControllerテストも参考にしてください。

RANDOM_PORTで実サーバーのHTTP通信を検証する

リダイレクトやCookieなど実際のHTTPを通さないと確認できないことを検証するときに使います。空きポートに自動割り当てされるので並列実行でも衝突しません。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiRandomPortTest {
    @LocalServerPort int port;
    @Autowired TestRestTemplate restTemplate;

    @Test
    void createsOrder() {
        var response = restTemplate.postForEntity(
            "http://localhost:" + port + "/api/orders", new OrderRequest("item-1"), OrderResponse.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }
}

WebTestClientで書きたい場合は@AutoConfigureWebTestClientを付けて注入します。レスポンスボディの検証もメソッドチェーンで書けます。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class OrderApiWebTestClientTest {
    @Autowired WebTestClient webTestClient;

    @Test
    void createsOrder() {
        webTestClient.post().uri("/api/orders")
            .bodyValue(new OrderRequest("item-1"))
            .exchange()
            .expectStatus().isCreated()
            .expectBody().jsonPath("$.status").isEqualTo("CREATED");
    }
}

詳しくはTestRestTemplateでのE2Eテストで扱っています。

DEFINED_PORTは固定ポートが必要なときだけ使う

外部システムが決まったポート番号を前提に叩いてくるような、限られたケースでだけDEFINED_PORTを使います。例えば決済代行サービスからのWebhookコールバック先を固定ポートで登録している場合です。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@TestPropertySource(properties = "server.port=8443")
class PaymentWebhookDefinedPortTest {
    @Autowired TestRestTemplate restTemplate;

    @Test
    void acceptsCallbackOnFixedPort() {
        var response = restTemplate.postForEntity(
            "http://localhost:8443/api/webhooks/payment", new WebhookPayload("paid"), Void.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

並列実行で同じポートを使うテストが衝突しやすいのがデメリットで、多くの場合はRANDOM_PORTで代替できるため採用は最小限にしましょう。

NONEはWeb環境を一切起動しない

サービス層やリポジトリ層だけの連携を検証するならNONEが最も軽量です。MockMvcもTestRestTemplateもWebTestClientも使えません。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class OrderServiceNoneTest {
    @Autowired OrderService orderService;

    @Test
    void calculatesTotalPrice() {
        Order order = orderService.create(new OrderRequest("item-1"));
        assertThat(order.getTotalPrice()).isEqualTo(1000);
    }
}

Webレイヤーを使わない検証にMOCKを選ぶと、不要にモックサーブレット環境を起動するだけ損です。この場合はNONEを優先しましょう。

@MockitoBeanで外部依存を差し替える

@MockitoBeanはSpring Framework 6.2(Spring Boot 3.4)以降で導入されたアノテーションで、非推奨になった@MockBeanの後継です。それより古いバージョンを使っている場合はここだけ@MockBeanに読み替えてください。

外部APIやDBアクセスをモック化できれば、実サーバーを起動しなくても検証できるケースが増えます。

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerWithExternalApiTest {
    @MockitoBean PaymentGatewayClient paymentGatewayClient;
    @Autowired MockMvc mockMvc;

    @Test
    void returnsPaymentFailure() throws Exception {
        when(paymentGatewayClient.charge(any())).thenThrow(new PaymentDeclinedException());
        mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isPaymentRequired());
    }
}

実サーバーが本当に必要かを見極める基準は、ネットワーク越しのHTTP挙動そのものを検証したいかどうかです。外部依存の結果だけ差し替えればいいなら、@MockitoBeanとMOCKの組み合わせで十分なことがほとんどです。

@Nestedでシナリオごとに整理する

webEnvironmentやモックの設定が異なるシナリオが増えたら、@Nestedで分割すると読みやすくなります。

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerScenarioTest {
    @Autowired MockMvc mockMvc;

    @Nested
    class WhenPaymentSucceeds {
        @MockitoBean PaymentGatewayClient client;

        @Test
        void returnsCreated() throws Exception {
            when(client.charge(any())).thenReturn(new PaymentResult("OK"));
            mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isCreated());
        }
    }

    @Nested
    class WhenPaymentFails {
        @MockitoBean PaymentGatewayClient client;

        @Test
        void returnsPaymentRequired() throws Exception {
            when(client.charge(any())).thenThrow(new PaymentDeclinedException());
            mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isPaymentRequired());
        }
    }
}

正常系と異常系のモック設定を1クラスに混在させるより、@Nestedで目的別に分けたほうが見通しが良くなります。

実サーバー起動が本当に必要なケース

RANDOM_PORT・DEFINED_PORTを使うべきなのは、次のような場合に限られます。

  • リダイレクトやCookie、レスポンスヘッダーなど、実際のネットワークを経由したHTTPクライアントの挙動そのものを検証したい
  • Spring Bootのデフォルトのエラーページ転送(/errorへの内部フォワード)など、サーブレットコンテナのエラーハンドリング機構に依存する挙動を確認したい(Filter Bean自体の挙動は@AutoConfigureMockMvcがデフォルトで全て適用するため、フィルタの検証だけならMOCKで十分です)
  • 外部システムとの実際のポート・プロトコル前提の連携を確認したい

それ以外の大半のケース、業務ロジックやController層の入出力検証はMOCKかNONEで十分です。

まとめ

webEnvironmentを選ぶときは、Webレイヤーを使わないならNONE、Controller層までならMOCK、実HTTP挙動の検証が必要ならRANDOM_PORTという優先順位で考えてみてください。外部依存を@MockitoBeanで先に差し替えておくと、より軽量なモードを選べる余地が広がります。

アノテーションの全体像を見直したいときはテストアノテーション使い分け早見表、DBまで通した結合テストが必要ならTestcontainersでの結合テストも合わせて確認してみてください。