MockMvcでのControllerテストは書けるようになったけど、「本当にHTTPを通した状態」で確認したい場面ってありますよね。フィルタの設定ミスや実際のシリアライズ結果、認証ヘッダーの扱いまで含めて確かめたいときは、MockMvcだけでは不安が残ります。

この記事では @SpringBootTest(webEnvironment = RANDOM_PORT) でアプリを実際に起動し、TestRestTemplate で本物のHTTPリクエストを送るテストの書き方を紹介します。JUnitとMockitoの基礎は身についている前提で進めます。

MockMvcだけでは検証できないこと

MockMvcは内部的に DispatcherServlet を直接呼び出す仕組みなので、実際のサーブレットコンテナやネットワークスタックは通りません。速くて便利な反面、以下のようなケースでは物足りなくなります。

  • Spring Securityのフィルタチェーンが本当に効いているか
  • 実際にシリアライズされたJSONのContent-Typeやヘッダー
  • 外部からアクセスした場合と同じ経路での挙動確認

こういう「実際にHTTPを通す」検証にはTestRestTemplateが向いています。

@SpringBootTest(RANDOM_PORT)でアプリを実起動する

webEnvironment に RANDOM_PORT を指定すると、アプリケーションが空いているポートで実際に起動します。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiE2ETest {

    @Autowired
    private TestRestTemplate restTemplate;

    @LocalServerPort
    private int port;
}

TestRestTemplate はSpring Bootが自動でBean登録してくれるので、@Autowired するだけで使えます。テストごとにポートが変わるので、並列実行してもポート競合が起きません。

@LocalServerPortでベースURLを組み立てる

ポート番号はテストのたびに変わるため、@LocalServerPort で受け取ってURLを組み立てます。

private String url(String path) {
    return "http://localhost:" + port + path;
}

こうしておくとリクエスト送信部分がすっきりします。

GETリクエストを送って検証する

まずは基本のGETから見ていきましょう。

@Test
void getUser_returns200AndBody() {
    ResponseEntity<UserDto> response =
        restTemplate.getForEntity(url("/api/users/1"), UserDto.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(response.getBody().getName()).isEqualTo("Taro");
}

ボディだけ欲しい場合は getForObject も使えますが、ステータスやヘッダーまで見たいなら getForEntity を使うのが基本です。

POST/PUT/DELETEを送る

更新系は postForEntity や exchange を使います。

@Test
void createUser_returns201() {
    CreateUserRequest req = new CreateUserRequest("Hanako");

    ResponseEntity<UserDto> response =
        restTemplate.postForEntity(url("/api/users"), req, UserDto.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    assertThat(response.getHeaders().getLocation().getPath()).contains("/api/users/");
}

@Test
void deleteUser_returns204() {
    restTemplate.exchange(url("/api/users/1"), HttpMethod.DELETE, null, Void.class);
}

PUTのようにボディとヘッダーの両方を細かく指定したい場合は、HttpEntity を作って exchange に渡します。

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UpdateUserRequest> entity = new HttpEntity<>(new UpdateUserRequest("Jiro"), headers);

ResponseEntity<UserDto> response =
    restTemplate.exchange(url("/api/users/1"), HttpMethod.PUT, entity, UserDto.class);

ヘッダーやボディの中身を細かく検証する

ResponseEntity からはステータス以外にヘッダーやボディも取得できるので、必要な項目をそのままアサーションできます。

assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(response.getBody().getEmail()).isNotBlank();

認証付きエンドポイントを叩く

Basic認証が必要なAPIには withBasicAuth が便利です。

@Test
void adminEndpoint_requiresAuth() {
    ResponseEntity<String> unauthorized =
        restTemplate.getForEntity(url("/api/admin"), String.class);
    assertThat(unauthorized.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);

    ResponseEntity<String> ok = restTemplate
        .withBasicAuth("admin", "password")
        .getForEntity(url("/api/admin"), String.class);
    assertThat(ok.getStatusCode()).isEqualTo(HttpStatus.OK);
}

BearerトークンのようにAuthorizationヘッダーを自分で組み立てたい場合は、HttpHeaders に設定して exchange で送ります。

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
restTemplate.exchange(url("/api/admin"), HttpMethod.GET, new HttpEntity<>(headers), String.class);

4xx/5xxエラーレスポンスを検証する

ここは意外と誤解されやすいポイントですが、TestRestTemplate は4xx/5xxが返ってきても例外を投げません。素直に ResponseEntity を受け取れます。

@Test
void getUser_notFound_returns404WithErrorBody() {
    ResponseEntity<ErrorResponse> response =
        restTemplate.getForEntity(url("/api/users/999"), ErrorResponse.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    assertThat(response.getBody().getMessage()).contains("見つかりません");
}

RestTemplate を素で使うと4xx/5xxで例外が飛びますが、TestRestTemplate はテスト用にエラーハンドリングがデフォルトで無効化されているためこの挙動になります。

MockMvcとTestRestTemplate、どちらを使うか

正直なところ、実行速度や書きやすさで言えばMockMvcの方が有利な場面が多いです。判断基準はシンプルで、「サーブレットコンテナやフィルタチェーンまで通す必要があるか」です。

  • Controllerのロジックだけ素早く確認したい → MockMvc
  • Spring Securityのフィルタや認証まわりを本番相当の経路で確認したい → TestRestTemplate
  • 実際のJSONシリアライズ結果やCORS設定まで見たい → TestRestTemplate

両方を併用して、Controller単体はMockMvcで、認証やフィルタが絡む重要なシナリオだけTestRestTemplateで確認する、という使い分けをしているプロジェクトも多いです。DBまで含めた結合テストが必要ならTestcontainersを使った結合テストも参考になります。

まとめ

@SpringBootTest(webEnvironment = RANDOM_PORT) と TestRestTemplate を組み合わせると、実際にアプリを起動した状態で本物のHTTPリクエストを送るテストが書けます。フィルタチェーンや認証まわりまで含めて確認したいときの強力な選択肢です。

まずは既存のMockMvcテストの中から、認証やヘッダー周りが絡むケースを1つTestRestTemplateに置き換えてみると、違いが体感しやすいと思います。アノテーションの全体像を整理したい方はテストアノテーション早見表もあわせてどうぞ。