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", "[email protected]");
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("[email protected]"));
}
静的インポートを追加しておくとすっきり書けます。
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", "[email protected]");
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("", "[email protected]");
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", "[email protected]"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk());
verify(userService, times(1)).findById(1L);
}
呼び出されていないことを確認したいときは verify(userService, never()).delete(any()) が使えます。
Spring Securityがある場合
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周りの詳細なテスト設定は別途確認してください。
まとめ
@WebMvcTest を使えば、DBや外部サービスを起動せずにControllerのHTTPレイヤーだけを素早くテストできます。@MockBean でServiceをモック化し、MockMvc の perform/andExpect チェーンで検証するパターンを身につけておくと、Controllerの品質がぐっと上がります。
DB接続が絡む結合テストは @SpringBootTestとTestcontainersの組み合わせ、外部APIのモックは WireMock も合わせて確認してみてください。