こんにちは、かつコーチです。
前回の記事で、@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, 中級者向け, テスト