【Spring Boot】@WebMvcTestでコントローラのテストを書く

Java

こんにちは、かつコーチです。

前回の記事で、@SpringBootTestはDIコンテナ全体を起動するため、テストの数が増えると実行が遅くなるという話をしました。
コントローラのリクエスト処理だけを確認したいときは、必要な部分だけを起動する@WebMvcTestを使うのが定石です。

筆者も最初は「コントローラのテストなんだから@SpringBootTestでいいのでは」と思っていましたが、@WebMvcTestに切り替えてから起動時間が体感でも明らかに速くなりました。
この記事では、@WebMvcTestの使い方と、MockMvcを使ったリクエスト・レスポンスの検証方法を解説します。

@WebMvcTestとは?

コントローラ層に絞ってBeanを起動する仕組み

@WebMvcTestは、MVCコンポーネント@Controller@RestController@ControllerAdviceなど、Webリクエストの処理に関わるBean)だけをDIコンテナに登録するアノテーションです。

@WebMvcTest(ArticleController.class)
class ArticleControllerTest {
    // ArticleControllerとMVC関連のBeanだけが起動する
}

対象のコントローラクラスを引数に指定すると、そのコントローラと関連するMVC設定のみが読み込まれます。
@Service@Repositoryが付いたBean、つまりDBアクセスを含む層はここでは起動されません。

なぜコントローラ層だけを分離してテストするのか

コントローラの責務は、リクエストを受け取り、適切なサービスを呼び出し、レスポンスを返すことです。
ビジネスロジックやDBアクセスの正しさは、サービス層やリポジトリ層のテストで別途確認すべき内容です。

@WebMvcTestで対象を絞ることで、以下のメリットがあります。

  • DB接続が不要なため、テスト実行が高速(数十〜数百ミリ秒単位)
  • 「コントローラのリクエストマッピングやバリデーションが正しいか」だけに集中してテストできる
  • サービス層の実装が未完成でも、モックを使ってコントローラのテストを先に書ける

基本の書き方・実装手順

MockMvcでリクエストを送る

@WebMvcTestを使うと、MockMvc(実際にHTTPサーバーを起動せずリクエストをシミュレートする仕組み)が自動でDIされます。

package com.example.blog.controller;

import com.example.blog.service.ArticleService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(ArticleController.class)
class ArticleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ArticleService articleService; // サービス層はモックに差し替える

    @Test
    void getArticle_returns200AndBody() throws Exception {
        given(articleService.findById(1L))
                .willReturn(new ArticleResponse(1L, "テスト記事", "本文です"));

        mockMvc.perform(get("/api/articles/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("テスト記事"));
    }
}

サービス層は@MockBeanでモックに差し替えているため、実際のDBアクセスは発生しません。
given(...).willReturn(...)でモックの返り値を設定し、コントローラが期待通りにレスポンスを組み立てているかを検証します。

POSTリクエストとリクエストボディの検証

フォーム送信やAPIリクエストのテストでは、JSONボディを含むPOSTリクエストを送る必要があります。

import com.fasterxml.jackson.databind.ObjectMapper;

@Autowired
private ObjectMapper objectMapper;

@Test
void createArticle_returns201() throws Exception {
    var request = new ArticleCreateRequest("新しい記事", "本文");
    given(articleService.create(any())).willReturn(new ArticleResponse(1L, "新しい記事", "本文"));

    mockMvc.perform(post("/api/articles")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1));
}

ObjectMapper(JavaオブジェクトとJSONを相互変換するJacksonのクラス)はSpring Bootが自動でBean登録してくれるため、@Autowiredでそのまま使えます。
objectMapper.writeValueAsString()でリクエストオブジェクトをJSON文字列に変換し、.content()に渡すのが基本形です。

ステータスコードとレスポンスヘッダーの検証

andExpectは複数チェーンできるため、ステータスコード・ヘッダー・ボディをまとめて検証できます。

mockMvc.perform(get("/api/articles/999"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.message").value("記事が見つかりません"));

存在しないIDへのリクエストで、コントローラ(または@ControllerAdvice)が正しく404を返しているかを確認できます。

よくあるつまずきポイント・エラー対処

❌ Before:@MockBeanを付け忘れてBean不足エラーになる

@WebMvcTestはサービス層のBeanを起動しないため、@MockBeanで明示的にモックを用意しないとコントローラの依存解決に失敗します。
筆者は最初、@MockBeanを書き忘れて以下のエラーに遭遇しました。

Error creating bean with name 'articleController':
Unsatisfied dependency expressed through constructor parameter 0:
No qualifying bean of type 'com.example.blog.service.ArticleService' available

コントローラがコンストラクタインジェクションでArticleServiceを要求しているのに、DIコンテナ内にBeanが1つも存在しないためのエラーです。

✅ After:依存するBeanをすべて@MockBeanで用意する

コントローラのコンストラクタが要求する依存は、すべて@MockBeanで用意します。

@WebMvcTest(ArticleController.class)
class ArticleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ArticleService articleService; // これを追加してエラーが解消した
}

コントローラが複数のサービスに依存している場合は、その数だけ@MockBeanを用意する必要があります。
依存が多くなりすぎてテストの準備が大変になってきたら、コントローラの責務が肥大化しているサインでもあるため、設計の見直しを検討する良いタイミングです。

応用・一歩先の使い方

Spring Securityが有効な場合の認証テスト

@WebMvcTestはデフォルトでSpring Securityの設定も一部読み込むため、認証が必要なエンドポイントは未認証だと401や403が返ってきます。

import org.springframework.security.test.context.support.WithMockUser;

@Test
@WithMockUser(username = "taro", roles = "USER")
void getMyArticles_returns200WhenAuthenticated() throws Exception {
    mockMvc.perform(get("/api/my-articles"))
            .andExpect(status().isOk());
}

spring-security-test@WithMockUserを使うと、実際にログイン処理を行わずに「認証済みユーザーとしてリクエストする」状態を再現できます。
権限(roles)ごとにアクセス制御が正しく機能しているかも、この方法で網羅的に確認できます。

バリデーションエラーのテスト

@Validを使ったリクエストのバリデーションも、@WebMvcTestで検証できます。

@Test
void createArticle_returns400WhenTitleIsBlank() throws Exception {
    var request = new ArticleCreateRequest("", "本文");

    mockMvc.perform(post("/api/articles")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isBadRequest());
}

タイトルが空文字のリクエストを送り、@NotBlankなどのバリデーションが正しく400を返しているかを確認します。
サービス層のモックを呼び出す前にコントローラ側でバリデーションが弾いているため、articleService.create()が呼ばれていないことをverify(articleService, never())...で確認するとより堅牢なテストになります。

まとめ

この記事のポイント

  • @WebMvcTestはコントローラ層に関わるBeanだけをDIコンテナに登録する
  • MockMvcを使うと、実際のHTTPサーバーを起動せずリクエストをシミュレートできる
  • サービス層は@MockBeanでモックに差し替えるため、DBアクセスは発生しない
  • 依存するBeanをすべて@MockBeanで用意しないと、Bean不足エラーが発生する
  • @WithMockUserで認証済み状態を再現し、アクセス制御のテストもできる

次に読むべき記事

リポジトリ層のテストについては、次の「@DataJpaTestでリポジトリのテストを書く」で解説します。

タグ: Spring Boot, 中級者向け, テスト

タイトルとURLをコピーしました