Chuyển đến nội dung chính

レッスン 19: API テスト — MockMvc、WebTestClient、REST を保証

同期 API テスト用の MockMvc。リアクティブ/非リアクティブの WebTestClient。 BDD スタイルの API テストでは REST が保証されます。契約テストのパターン。

💻 プログラミング — レッスン 18 レッスン 19: API テスト — MockMvc、 WebTestClient と REST を保証

Spring Boot 4: 基本から上級まで

パート 5: テストとコードの品質

xdev.asia

はじめに

API テストにより、HTTP リクエストからレスポンスまでエンドポイントが適切に機能することが保証されます。この記事では、MockMvc (軽量)、WebTestClient (最新)、および REST Assured (BDD スタイル) の 3 つのアプローチを比較します。


1. MockMvc — 軽量 API テスト

1.1 セットアップ

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private ProductService productService;

    @Autowired
    private ObjectMapper objectMapper;
}

1.2 GET リクエストのテスト

@Test
void shouldReturnProductById() throws Exception {
    var product = new ProductResponse(1L, "Book", new BigDecimal("450000"), "Education");
    when(productService.getProduct(1L)).thenReturn(product);

    mockMvc.perform(get("/api/v1/products/{id}", 1L)
            .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath("$.id").value(1))
        .andExpect(jsonPath("$.name").value("Book"))
        .andExpect(jsonPath("$.price").value(450000));
}

@Test
void shouldReturn404WhenProductNotFound() throws Exception {
    when(productService.getProduct(999L))
        .thenThrow(new ResourceNotFoundException("Product", "id", 999L));

    mockMvc.perform(get("/api/v1/products/999"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.detail").value("Product not found with id: 999"));
}

1.3 POST リクエストのテスト

@Test
void shouldCreateProduct() throws Exception {
    var request = new CreateProductRequest("New Book", new BigDecimal("300000"), "Education");
    var response = new ProductResponse(1L, "New Book", new BigDecimal("300000"), "Education");
    when(productService.create(any())).thenReturn(response);

    mockMvc.perform(post("/api/v1/products")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.id").value(1))
        .andExpect(jsonPath("$.name").value("New Book"));
}

@Test
void shouldReturn400WhenInvalidRequest() throws Exception {
    var request = new CreateProductRequest("", null, "");  // Invalid

    mockMvc.perform(post("/api/v1/products")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.errors").isArray())
        .andExpect(jsonPath("$.errors.length()").value(greaterThan(0)));
}

1.4 認証を使用したテスト

@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void shouldDeleteProductAsAdmin() throws Exception {
    mockMvc.perform(delete("/api/v1/products/1"))
        .andExpect(status().isNoContent());

    verify(productService).deleteProduct(1L);
}

@Test
@WithMockUser(username = "user", roles = {"USER"})
void shouldForbidDeleteForNormalUser() throws Exception {
    mockMvc.perform(delete("/api/v1/products/1"))
        .andExpect(status().isForbidden());
}

2. WebTestClient

2.1 セットアップ

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApiTest {

    @Autowired
    private WebTestClient webTestClient;
}

2.2 流暢な API

@Test
void shouldGetAllProducts() {
    webTestClient.get()
        .uri("/api/v1/products?page=0&size=10")
        .accept(MediaType.APPLICATION_JSON)
        .exchange()
        .expectStatus().isOk()
        .expectHeader().contentType(MediaType.APPLICATION_JSON)
        .expectBody()
        .jsonPath("$.content").isArray()
        .jsonPath("$.content.length()").isEqualTo(3)
        .jsonPath("$.totalElements").isEqualTo(3);
}

@Test
void shouldCreateProductAndReturn201() {
    var request = new CreateProductRequest("WebTestClient Book",
        new BigDecimal("500000"), "Education");

    webTestClient.post()
        .uri("/api/v1/products")
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(request)
        .exchange()
        .expectStatus().isCreated()
        .expectBody(ProductResponse.class)
        .value(response -> {
            assertThat(response.id()).isNotNull();
            assertThat(response.name()).isEqualTo("WebTestClient Book");
        });
}

3. REST 保証 — BDD スタイル

3.1 セットアップ

// build.gradle.kts
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.rest-assured:spring-mock-mvc")

3.2 いつ与えられるか

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductRestAssuredTest {

    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
        RestAssured.basePath = "/api/v1";
    }

    @Test
    void shouldGetProduct() {
        given()
            .accept(ContentType.JSON)
        .when()
            .get("/products/{id}", 1)
        .then()
            .statusCode(200)
            .body("name", equalTo("Book"))
            .body("price", equalTo(450000));
    }

    @Test
    void shouldCreateProduct() {
        var request = Map.of(
            "name", "REST Assured Book",
            "price", 350000,
            "category", "Education"
        );

        given()
            .contentType(ContentType.JSON)
            .body(request)
        .when()
            .post("/products")
        .then()
            .statusCode(201)
            .body("id", notNullValue())
            .body("name", equalTo("REST Assured Book"));
    }
}

4. 3 つのアプローチを比較する

特長モックMVCウェブテストクライアント安心の休息
スピード最速 (サーバーなし)サーバーが必要サーバーが必要
スタイルビルダーパターン滑らかな反応性BDD がいつ与えられるか
サーブレット/リアクティブサーブレットのみ両方サーブレットのみ
Spring Boot のデフォルトはいはいさらに依存関係が必要
こんな方に最適単体テストコントローラー統合テストBDD、読み取り可能なテスト

概要

  • MockMvc: 軽量、サーバーを起動する必要がなく、@WebMvcTest を使用した単体テストのコントローラー層に適しています
  • WebTestClient: 最新の流暢な API、サーブレットとリアクティブの両方をサポートし、統合テストに適しています
  • REST 保証: BDD の Given-When-Then スタイル、読みやすいテスト コード、API 受け入れテストに適しています
  • @WithMockUser を組み合わせてコントローラー レベルで認可をテストします

演習

  1. ProductController の MockMvc テストを作成します: 検証エラーと 404 を含む GET、POST、PUT、DELETE のテスト
  2. 完全な注文フローの WebTestClient 統合テストを作成します: 作成 → 取得 → 更新 → 削除
  3. 認証フローの REST Assured テストを実装します: ログイン → JWT の取得 → 保護されたエンドポイントにアクセス