Controllerのテストを書こうとして @SpringBootTest を使ったら、DBの設定が必要になったり起動が遅くてストレスを感じたことはありませんか。HTTPレイヤーだけ検証したいなら、@WebMvcTest を使うと もっとシンプルかつ高速にテストが書けます。
今回は @WebMvcTest と MockMvc を組み合わせたController専用テストの書き方を、実装例を交えて紹介します。JUnitとMockitoの基礎知識があれば読み進められます(基礎は JUnitとMockitoを使ったSpring Bootのテスト入門 を参照してください)。
@WebMvcTestと@SpringBootTestの違い
@SpringBootTest はアプリ全体のコンテキストを起動するので、DBやメッセージキューなども含めて全部立ち上がります。結合テストには便利ですが、Controllerだけ確認したいときは明らかにやり過ぎですよね。
@WebMvcTest はWeb層(Controller・Filter・Converterなど)だけを起動します。ServiceやRepositoryはコンテキストに含まれないので @MockBean でモック化します。
| @SpringBootTest | @WebMvcTest | |
|---|---|---|
| 起動範囲 | アプリ全体 | Web層のみ |
| 速度 | 遅い | 速い |
| DB接続 | 必要になりがち | 不要 |
| 用途 | 結合テスト | Controller単体テスト |
HTTPレイヤーだけ検証したいなら @WebMvcTest、DB含む結合テストなら @SpringBootTest と Testcontainers の組み合わせが適しています。
テスト対象のControllerを準備する
まずはテスト対象となるシンプルな UserController を用意します。
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<Void> createUser(@Valid @RequestBody UserRequest request) {
Long id = userService.create(request);
return ResponseEntity.created(URI.create("/users/" + id)).build();
}
}
@WebMvcTestの基本セットアップ
テストクラスはこんな感じで書きます。
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
UserService userService;
}
ポイントは3つです。
@WebMvcTest(UserController.class)でテスト対象のControllerを指定しますMockMvcは@WebMvcTest使用時に自動設定されるので@Autowiredで注入するだけですUserServiceは@MockBeanでモック化します。コンテキストにServiceが含まれないため、これがないと起動時にNoSuchBeanDefinitionExceptionが発生します
GETリクエストのテスト
perform() にリクエストを渡して、andExpect() でチェーンしながら検証します。
@Test
void ユーザーをIDで取得できる() throws Exception {
// Arrange
UserResponse user = new UserResponse(1L, "Taro", "taro@example.com");
when(userService.findById(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Taro"))
.andExpect(jsonPath("$.email").value("taro@example.com"));
}
静的インポートを追加しておくとすっきり書けます。
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.*;
jsonPath はネストしたフィールドも指定できます。配列要素なら $.items[0].name、リストの件数確認なら jsonPath("$.items", hasSize(3)) が便利です。
POSTリクエストのテスト
リクエストボディを送る場合は .content() と .contentType() を追加します。
@Test
void ユーザーを作成できる() throws Exception {
// Arrange
UserRequest request = new UserRequest("Hanako", "hanako@example.com");
when(userService.create(any())).thenReturn(2L);
// Act & Assert
mockMvc.perform(post("/users")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/users/2"));
}
ObjectMapper は @WebMvcTest のコンテキストで自動提供されるので @Autowired で注入できます。
バリデーションエラーのテスト(400 Bad Request)
@Valid で入力検証を設定している場合、不正なリクエストを送ると400が返ります。このパターンも書いておきましょう。
@Test
void 不正なリクエストは400を返す() throws Exception {
// nameが空でバリデーションエラー
UserRequest request = new UserRequest("", "hanako@example.com");
mockMvc.perform(post("/users")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
独自のエラーレスポンス形式を設定している場合は jsonPath("$.errors[0].field").value("name") のようにフィールドごとに検証できます(RESTのExceptionHandlerはこちら)。カスタムバリデーションアノテーションについては こちらの記事 も参考にしてください。
Serviceの呼び出しを検証する
ControllerがServiceを正しく呼び出しているか verify() で確認できます。
@Test
void ServiceのfindByIdを1回呼び出す() throws Exception {
when(userService.findById(1L))
.thenReturn(new UserResponse(1L, "Taro", "taro@example.com"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk());
verify(userService, times(1)).findById(1L);
}
呼び出されていないことを確認したいときは verify(userService, never()).delete(any()) が使えます。
Spring Securityがある場合
print() でレスポンスをデバッグする
テストが期待どおり通らないときは MockMvcResultHandlers.print() を andDo() に渡すと、リクエスト/レスポンスの全文がコンソールに出力されます。原因調査の第一歩として非常に便利です。
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
mockMvc.perform(get("/users/1"))
.andDo(print())
.andExpect(status().isOk());
ヘッダ・ボディ・ハンドラ情報まで一覧できるので、jsonPath のパスを書き間違えたときの確認にも使えます。
ファイルアップロード(Multipart)のテスト
multipart() と MockMultipartFile を組み合わせるとファイルアップロードのテストが書けます。
@Test
void ファイルをアップロードできる() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "avatar.png", MediaType.IMAGE_PNG_VALUE, "dummy-bytes".getBytes());
mockMvc.perform(multipart("/users/1/avatar").file(file).with(csrf()))
.andExpect(status().isOk());
}
リクエストパラメータも一緒に送りたい場合は .param("key", "value") をチェーンします。
CSRFトークンが必要なPOST/PUT/DELETEのテスト
Spring Securityを有効にした状態で POST PUT DELETE をテストすると、CSRFトークン不足で 403 Forbidden が返ります。.with(csrf()) を付けるとCSRFトークン付きリクエストを送れます。
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@Test
@WithMockUser
void CSRFトークン付きでPOSTできる() throws Exception {
mockMvc.perform(post("/users")
.with(csrf())
.content(objectMapper.writeValueAsString(new UserRequest("Hanako", "hanako@example.com")))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated());
}
API専用でCSRFを無効化している場合は不要ですが、@WebMvcTest は Security の自動設定を読み込むため、デフォルト構成では明示的に .with(csrf()) を付けるのが安全です。
spring-security-test を追加すると @WithMockUser で認証済みユーザーとしてリクエストを送れます。
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void 管理者は200を受け取る() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpect(status().isOk());
}
@WebMvcTest はSecurityの自動設定も読み込むので、未認証でアクセスすると401や302が返ります。Security周りの詳細なテスト設定は別途確認してください。
よくあるエラーと対処
| エラー | 主な原因 | 対処 |
|---|---|---|
NoSuchBeanDefinitionException: UserService | @MockBean でService等を用意していない | テストクラスに @MockBean UserService userService; を追加 |
403 Forbidden(POST/PUT/DELETE) | Security有効時にCSRFトークンが無い | .with(csrf()) を付ける |
401 Unauthorized | 未認証でアクセスしている | @WithMockUser を付与する、または該当エンドポイントを permitAll() に |
MediaType not supported (415) | contentType() 未指定 / 値が不一致 | .contentType(MediaType.APPLICATION_JSON) を明示 |
jsonPath が null を返す | レスポンス構造とパスが不一致 | andDo(print()) でレスポンスを確認し、パスを修正 |
チートシート
| やりたいこと | 書き方 |
|---|---|
| GET | mockMvc.perform(get("/path")) |
| POST(JSON) | .contentType(MediaType.APPLICATION_JSON).content(json) |
| クエリパラメータ | .param("key", "value") |
| ステータス検証 | .andExpect(status().isOk()) |
| JSON検証 | .andExpect(jsonPath("$.field").value(...)) |
| 配列件数 | .andExpect(jsonPath("$.items", hasSize(3))) |
| ヘッダ検証 | .andExpect(header().string("Location", "...")) |
| 認証済みユーザー | @WithMockUser(roles = "ADMIN") |
| CSRF付与 | .with(csrf()) |
| デバッグ出力 | .andDo(print()) |
まとめ
@WebMvcTest を使えば、DBや外部サービスを起動せずにControllerのHTTPレイヤーだけを素早くテストできます。@MockBean でServiceをモック化し、MockMvc の perform/andExpect チェーンで検証するパターンを身につけておくと、Controllerの品質がぐっと上がります。
DB接続が絡む結合テストは @SpringBootTestとTestcontainersの組み合わせ、外部APIのモックは WireMock も合わせて確認してみてください。
関連記事
- Spring BootのJackson設定でJSONシリアライズをカスタマイズする -
jsonPathで検証する前提となる、レスポンスJSONの形式設計を整理しておくと検証ロジックが安定します。 - Spring Bootでカスタムバリデーションアノテーションを作る方法 -
@Validで発生する400レスポンスを独自ルールで返したいときに役立ちます。