Controllerのテストを書こうとして @SpringBootTest を使ったら、DBの設定が必要になったり起動が遅くてストレスを感じたことはありませんか。HTTPレイヤーだけ検証したいなら、@WebMvcTest を使うと もっとシンプルかつ高速にテストが書けます。

今回は @WebMvcTestMockMvc を組み合わせた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含む結合テストなら @SpringBootTestTestcontainers の組み合わせが適しています。

テスト対象の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をモック化し、MockMvcperform/andExpect チェーンで検証するパターンを身につけておくと、Controllerの品質がぐっと上がります。

DB接続が絡む結合テストは @SpringBootTestとTestcontainersの組み合わせ、外部APIのモックは WireMock も合わせて確認してみてください。