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 | しない | MockMvc | Controller層の単体検証 | 低 |
| 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での結合テストも合わせて確認してみてください。