Testing REST APIs is one of the most common tasks in any Spring Boot project. JUnit 6 supports two primary approaches: MockMvc for servlet-based (synchronous) APIs and WebTestClient for reactive (WebFlux) APIs — though WebTestClient can also test traditional Spring MVC applications. This guide covers both tools in depth with real request/response examples, JSON path assertions, and patterns for testing authentication, error responses, and pagination.
MockMvc vs WebTestClient: When to Use Which
| MockMvc | WebTestClient | |
|---|---|---|
| Transport | No real HTTP — tests the servlet layer directly | Real or mock HTTP |
| Best for | Spring MVC (traditional Servlet) | WebFlux (reactive) or both |
| Speed | Fastest — no networking overhead | Slightly slower but more realistic |
| Fluent API | Builder-style with matchers | Reactive, fluent chain |
| Works in @WebMvcTest | ✅ Yes (auto-configured) | ✅ Yes (with @AutoConfigureMockMvc) |
Part 1: Testing REST APIs with MockMvc
Setup and Auto-Configuration
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
@SpringBootTest
@AutoConfigureMockMvc // injects MockMvc with full security and filters
@DisplayName("Product API — MockMvc tests")
class ProductApiMockMvcTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ProductRepository productRepository; // used for test data setup
@BeforeEach
void seedTestData() {
productRepository.deleteAll();
productRepository.saveAll(List.of(
new Product(null, "Laptop", 999.00, "Electronics"),
new Product(null, "Headphones", 79.00, "Electronics"),
new Product(null, "Notebook", 5.99, "Stationery")
));
}
@Test
@DisplayName("GET /api/products returns 200 with all products")
void getAllProductsReturns200() throws Exception {
mockMvc.perform(get("/api/products")
.accept("application/json"))
.andDo(print()) // prints request + response to console for debugging
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$.length()").value(3))
.andExpect(jsonPath("$[0].name").value("Laptop"));
}
}
Testing POST, PUT, DELETE
import com.fasterxml.jackson.databind.ObjectMapper;
@SpringBootTest
@AutoConfigureMockMvc
class ProductCrudTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper; // for serialising request body
@Test
@DisplayName("POST /api/products creates product and returns 201")
void createProductReturns201() throws Exception {
// Arrange: build request body as a Java object, serialise to JSON
CreateProductRequest request =
new CreateProductRequest("Keyboard", 49.99, "Electronics");
String requestJson = objectMapper.writeValueAsString(request);
mockMvc.perform(post("/api/products")
.contentType("application/json")
.content(requestJson))
.andExpect(status().isCreated())
.andExpect(header().exists("Location")) // REST convention: Location header on 201
.andExpect(jsonPath("$.id").isNotEmpty())
.andExpect(jsonPath("$.name").value("Keyboard"))
.andExpect(jsonPath("$.price").value(49.99));
}
@Test
@DisplayName("PUT /api/products/{id} updates product and returns 200")
void updateProductReturns200() throws Exception {
// Seed a product first
Product existing = productRepository.save(
new Product(null, "OldName", 10.0, "Electronics"));
UpdateProductRequest updateRequest =
new UpdateProductRequest("NewName", 20.0, "Electronics");
mockMvc.perform(put("/api/products/" + existing.getId())
.contentType("application/json")
.content(objectMapper.writeValueAsString(updateRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("NewName"))
.andExpect(jsonPath("$.price").value(20.0));
}
@Test
@DisplayName("DELETE /api/products/{id} removes product and returns 204")
void deleteProductReturns204() throws Exception {
Product existing = productRepository.save(
new Product(null, "ToDelete", 5.0, "Misc"));
mockMvc.perform(delete("/api/products/" + existing.getId()))
.andExpect(status().isNoContent());
// Verify actually deleted
mockMvc.perform(get("/api/products/" + existing.getId()))
.andExpect(status().isNotFound());
}
}
Testing Validation Errors
@Test
@DisplayName("POST /api/products returns 400 with validation errors for blank name")
void createProductWithBlankNameReturns400() throws Exception {
// Send a product with blank name — should fail validation
String invalidJson = "{"name":"","price":49.99,"category":"Electronics"}";
mockMvc.perform(post("/api/products")
.contentType("application/json")
.content(invalidJson))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[0].field").value("name"))
.andExpect(jsonPath("$.errors[0].message").value("must not be blank"));
}
Part 2: Testing REST APIs with WebTestClient
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DisplayName("Product API — WebTestClient integration tests")
class ProductApiWebTestClientTest {
@Autowired
private WebTestClient webTestClient;
@Test
@DisplayName("GET /api/products returns 200 and a list of products")
void getAllProductsReturns200() {
webTestClient.get()
.uri("/api/products")
.accept(MediaType.APPLICATION_JSON)
.exchange() // execute the request
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(ProductDto.class) // deserialise response body to List
.hasSize(3)
.contains(new ProductDto("Laptop", 999.00));
}
@Test
@DisplayName("POST /api/products creates a product and returns 201")
void createProductReturns201() {
CreateProductRequest request =
new CreateProductRequest("Mouse", 29.99, "Electronics");
webTestClient.post()
.uri("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request) // automatically serialised to JSON
.exchange()
.expectStatus().isCreated()
.expectBody(ProductDto.class)
.value(product -> {
// Use AssertJ or standard assertions on the response body
assertNotNull(product.getId());
assertEquals("Mouse", product.getName());
assertEquals(29.99, product.getPrice(), 0.001);
});
}
@Test
@DisplayName("GET /api/products/{id} returns 404 for non-existent product")
void getNonExistentProductReturns404() {
webTestClient.get()
.uri("/api/products/99999")
.exchange()
.expectStatus().isNotFound();
}
}
MockMvc Result Matchers Quick Reference
| Matcher | What it checks |
|---|---|
status().isOk() | HTTP 200 |
status().isCreated() | HTTP 201 |
status().isBadRequest() | HTTP 400 |
status().isNotFound() | HTTP 404 |
status().is(403) | Specific status code |
content().contentType(...) | Content-Type header |
jsonPath("$.field").value(x) | JSON field equals value |
jsonPath("$").isArray() | Root is a JSON array |
jsonPath("$.length()").value(n) | Array has n elements |
header().exists("Location") | Response header exists |
header().string("X-Req-Id", "123") | Response header value |
Frequently Asked Questions (FAQs)
Q1: Should I use MockMvc or WebTestClient for Spring MVC tests?
Both work for Spring MVC. MockMvc is the traditional, well-established choice with excellent Spring Test integration and no networking overhead. WebTestClient offers a more fluent, modern API and can be used for both Spring MVC and WebFlux. For new projects, WebTestClient is increasingly preferred because it supports both paradigms with one API. For existing projects with heavy MockMvc investment, continue using it — migration is optional.
Q2: How do I test secured endpoints with MockMvc?
Add the spring-security-test dependency and use @WithMockUser or SecurityMockMvcRequestPostProcessors.jwt() on your mock request. For example: mockMvc.perform(get("/api/admin").with(user("admin").roles("ADMIN"))). This simulates an authenticated user without requiring a real authentication flow.
Q3: What is the difference between andDo(print()) and andReturn()?
andDo(print()) prints the full request and response to the console — extremely useful during debugging but should be removed from production test code. andReturn() returns an MvcResult object that lets you access the response body, headers, and status for custom assertions beyond what the built-in matchers support.
Q4: How do I test file upload endpoints with MockMvc?
Use MockMvcRequestBuilders.multipart() combined with MockMultipartFile:
MockMultipartFile file = new MockMultipartFile(
"file", "test.csv", "text/csv", "name,pricenLaptop,999".getBytes());
mockMvc.perform(multipart("/api/products/import").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.imported").value(1));
Q5: How do I assert response body as a plain String in MockMvc?
Use content().string("expected string") for exact match, or content().string(containsString("partial")) with a Hamcrest matcher for partial matching. For large JSON responses where jsonPath assertions become tedious, consider deserializing with andReturn().getResponse().getContentAsString() and then using Jackson’s ObjectMapper for full object comparison.
See Also
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
- JUnit 6 with Mockito: Mocking, Spying, and Best Practices
- JUnit 6 with Testcontainers: Real Database Integration Testing
- JUnit 6 Assertions: All Methods Explained
- JUnit 6 Tutorial: Complete Series Index
Conclusion
MockMvc and WebTestClient are both excellent tools for REST API testing in Spring Boot. MockMvc is fast, battle-tested, and tightly integrated with Spring’s servlet layer. WebTestClient offers a modern fluent API that works across both Spring MVC and WebFlux. Use MockMvc in @WebMvcTest for fast, isolated controller slice tests, and WebTestClient in @SpringBootTest for end-to-end HTTP integration tests against a running server.
Next: JUnit 6 with Mockito — master mocking, stubbing, spying, and verification to isolate your units completely in any test scenario.