はじめに
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 を組み合わせてコントローラー レベルで認可をテストします
演習
- ProductController の MockMvc テストを作成します: 検証エラーと 404 を含む GET、POST、PUT、DELETE のテスト
- 完全な注文フローの WebTestClient 統合テストを作成します: 作成 → 取得 → 更新 → 削除
- 認証フローの REST Assured テストを実装します: ログイン → JWT の取得 → 保護されたエンドポイントにアクセス