mcpField
U
unit-test-controller-layer

unit-test-controller-layer

github.com/giuseppe-trisciuoglioテスト・品質3653.8k 回インストール7 週間前に更新
リポジトリ

unit-test-controller-layer について

Spring MVC 開発者向けに、MockMvc とモックしたサービスを使ってコントローラーを分離してテストするスキルです。リクエスト、レスポンス、検証、ヘッダー、コンテンツネゴシエーション、エラー処理を扱います。

主な機能

  1. 01MockMvc による単独コントローラー設定
  2. 02サービス依存関係のモック化
  3. 03HTTP メソッドとステータスの検証
  4. 04JSON とレスポンス本文の確認
  5. 05検証、ヘッダー、エラーケースのテスト
  6. 06コンテンツネゴシエーションのテスト

活用シーン

  1. 01REST コントローラーのエンドポイントテスト
  2. 02リクエストとレスポンス処理の確認
  3. 03検証結果とエラーレスポンスの確認
  4. 04ヘッダーとコンテンツタイプのテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-controller-layer

インストール後、Claude Code で/unit-test-controller-layerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
advanced.md
common-pitfalls.md
troubleshooting.md

詳細

MockMvc を使用した REST コントローラーのユニットテスト

概要

MockMvc を使用して、@RestController および @Controller クラスをユニットテストするパターンを紹介します。リクエスト/レスポンスの処理、HTTP ステータスコード、リクエストパラメーターのバインド、バリデーション、コンテンツネゴシエーション、レスポンスヘッダー、モックしたサービス依存関係を使用した例外処理を扱います。

使用する場面

次の場合に使用します:コントローラーテスト、API エンドポイントのテスト、Spring MVC テスト、HTTP リクエストのモック、Web レイヤーのエンドポイントのユニットテスト、REST コントローラーを分離して検証する場合。

手順

  1. スタンドアロンの MockMvc をセットアップする:分離してテストするには MockMvcBuilders.standaloneSetup(controller) を使用します
  2. サービス依存関係をモックする:すべてのサービスに @Mock を使用し、コントローラーには @InjectMocks を使用します
  3. HTTP メソッドをテストする:GET、POST、PUT、PATCH、DELETE と適切なステータスコードをテストします
  4. レスポンスを検証する:JSON には JsonPath アサーションを、本文にはコンテンツマッチャーを使用します
  5. バリデーションをテストする:無効な入力を送信し、エラーの詳細を含む 400 ステータスを確認します
  6. エラーをテストする:適切な条件で 404、400、401、403、500 を確認します
  7. ヘッダーを検証する:リクエスト(Authorization)ヘッダーとレスポンスヘッダーの両方を検証します
  8. コンテンツネゴシエーションをテストする:異なる Accept ヘッダーと Content-Type ヘッダーを使用します
バリデーションのワークフロー
Run test → If fails: add .andDo(print()) → Check actual vs expected → Fix assertion

例

Maven / Gradle の依存関係
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
基本パターン:GET エンドポイント
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(MockitoExtension.class)
class UserControllerTest {

  @Mock
  private UserService userService;

  @InjectMocks
  private UserController userController;

  private MockMvc mockMvc;

  @BeforeEach
  void setUp() {
    mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
  }

  @Test
  void shouldReturnAllUsers() throws Exception {
    List<UserDto> users = List.of(new UserDto(1L, "Alice"), new UserDto(2L, "Bob"));
    when(userService.getAllUsers()).thenReturn(users);

    mockMvc.perform(get("/api/users"))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$[0].id").value(1))
      .andExpect(jsonPath("$[0].name").value("Alice"));

    verify(userService, times(1)).getAllUsers();
  }

  @Test
  void shouldReturn404WhenUserNotFound() throws Exception {
    when(userService.getUserById(999L))
      .thenThrow(new UserNotFoundException("User not found"));

    mockMvc.perform(get("/api/users/999"))
      .andExpect(status().isNotFound());

    verify(userService).getUserById(999L);
  }
}
POST:リソースの作成
@Test
void shouldCreateUserAndReturn201() throws Exception {
  UserDto createdUser = new UserDto(1L, "Alice", "[email protected]");
  when(userService.createUser(any())).thenReturn(createdUser);

  mockMvc.perform(post("/api/users")
      .contentType("application/json")
      .content("{\"name\":\"Alice\",\"email\":\"[email protected]\"}"))
    .andExpect(status().isCreated())
    .andExpect(jsonPath("$.id").value(1))
    .andExpect(jsonPath("$.name").value("Alice"));

  verify(userService).createUser(any(UserCreateRequest.class));
}
PUT:リソースの更新
@Test
void shouldUpdateUserAndReturn200() throws Exception {
  UserDto updatedUser = new UserDto(1L, "Updated");
  when(userService.updateUser(eq(1L), any())).thenReturn(updatedUser);

  mockMvc.perform(put("/api/users/1")
      .contentType("application/json")
      .content("{\"name\":\"Updated\"}"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.name").value("Updated"));

  verify(userService).updateUser(eq(1L), any());
}
DELETE:リソースの削除
@Test
void shouldDeleteUserAndReturn204() throws Exception {
  doNothing().when(userService).deleteUser(1L);

  mockMvc.perform(delete("/api/users/1"))
    .andExpect(status().isNoContent());

  verify(userService).deleteUser(1L);
}
クエリパラメーター
@Test
void shouldFilterUsersByName() throws Exception {
  when(userService.searchUsers("Alice")).thenReturn(List.of(new UserDto(1L, "Alice")));

  mockMvc.perform(get("/api/users/search").param("name", "Alice"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$[0].name").value("Alice"));

  verify(userService).searchUsers("Alice");
}
パス変数
@Test
void shouldGetUserByIdFromPath() throws Exception {
  when(userService.getUserById(123L)).thenReturn(new UserDto(123L, "Alice"));

  mockMvc.perform(get("/api/users/{id}", 123L))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.id").value(123));
}
バリデーションエラー(400)
@Test
void shouldReturn400WhenRequestBodyInvalid() throws Exception {
  mockMvc.perform(post("/api/users")
      .contentType("application/json")
      .content("{\"name\":\"\"}"))
    .andExpect(status().isBadRequest())
    .andExpect(jsonPath("$.errors").isArray());
}
レスポンスヘッダー
@Test
void shouldReturnCustomHeaders() throws Exception {
  when(userService.getAllUsers()).thenReturn(List.of());

  mockMvc.perform(get("/api/users"))
    .andExpect(status().isOk())
    .andExpect(header().exists("X-Total-Count"))
    .andExpect(header().string("X-Total-Count", "0"));
}
Authorization ヘッダー
@Test
void shouldRequireAuthorizationHeader() throws Exception {
  mockMvc.perform(get("/api/users"))
    .andExpect(status().isUnauthorized());

  mockMvc.perform(get("/api/users").header("Authorization", "Bearer token"))
    .andExpect(status().isOk());
}
コンテンツネゴシエーション
@Test
void shouldReturnJsonWhenAcceptHeaderIsJson() throws Exception {
  when(userService.getUserById(1L)).thenReturn(new UserDto(1L, "Alice"));

  mockMvc.perform(get("/api/users/1").accept("application/json"))
    .andExpect(status().isOk())
    .andExpect(content().contentType("application/json"));
}

ベストプラクティス

  • コントローラーを分離してテストするには standaloneSetup() を使用します
  • サービスレイヤーをモックします。コントローラーは HTTP を処理し、サービスはビジネスロジックを処理します
  • モックの呼び出しを検証します:verify(service).method(args)
  • 正常系とエラーのシナリオ(404、400、500)の両方をテストします
  • 簡潔な JSON アサーションには jsonPath() を使用します
  • 各テストメソッドでは、焦点を絞ったアサーションを 1 つ使用します

制約と警告

  • コントローラーテストで検証するのは HTTP の処理のみであり、リクエストフロー全体ではありません
  • standaloneSetup() は、完全なコンテキストなしでは @Validated をサポートしない場合があります
  • JsonPath には、レスポンス本文に有効な JSON が必要です
  • @PreAuthorize/@Secured には追加のセットアップが必要です。セキュリティテストを別途実施することを検討してください
  • ファイルのアップロードには MockMultipartFile が必要です

参考資料

コメント