Spring MockMvc
Spring MockMvc has traditionally used Hamcrest matchers for assertions, which required mixing different assertion styles and was less discoverable.
Spring Framework 6.2 introduced MockMvcTester
, providing full AssertJ integration for more consistent and expressive controller tests.
Traditional Hamcrest matchers
MockMvc's traditional approach uses MockMvcResultMatchers
with Hamcrest-style matchers, which feels disconnected from modern assertion libraries.
- Before
- After
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class BundleControllerTest {
private final MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new BundleController()).build();
@Test
void getBundle() throws Exception {
mockMvc.perform(get("/bundle"))
.andExpectAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.books[0].title").value("Effective Java"));
}
@Test
void boom() throws Exception {
mockMvc.perform(get("/boom"))
.andExpect(status().isInternalServerError());
}
}
Traditional MockMvc assertions:
- Use Hamcrest-style matchers instead of AssertJ
- Require many static imports from
MockMvcResultMatchers
- Mix assertion styles when combining with other AssertJ assertions
- Less discoverable API compared to fluent AssertJ chains
- Force checked exception handling with
throws Exception
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import static org.assertj.core.api.Assertions.assertThat;
class BundleControllerTest {
private final MockMvcTester mockMvc = MockMvcTester.of(new BundleController());
@Test
void getBundle() {
assertThat(mockMvc.get().uri("/bundle"))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
.bodyJson()
.hasPathSatisfying("$.books[0].title",
title -> assertThat(title).isEqualTo("Effective Java"));
}
@Test
void boom() {
assertThat(mockMvc.get().uri("/boom"))
.hasStatus5xxServerError();
}
}
Spring Framework 6.2+ provides MockMvcTester
with full AssertJ integration:
- Consistent AssertJ fluent API across all assertions
- Single static import for
assertThat()
- Better IDE autocomplete and discoverability
- No checked exceptions to handle
- Cleaner, more readable test code
Bridge approach
For incremental migration, you can use MockMvcTester
while keeping existing MockMvcRequestBuilders
imports.
- Before
- After
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class BundleControllerTest {
private final MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new BundleController()).build();
@Test
void getBundle() throws Exception {
mockMvc.perform(get("/bundle"))
.andExpectAll(
status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.books[0].title").value("Effective Java"));
}
@Test
void boom() {
assertThat(mockMvc.perform(get("/boom")))
.hasStatus5xxServerError();
}
}
Starting with traditional MockMvc and Hamcrest matchers.
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
class BundleControllerTest {
private final MockMvcTester mockMvc = MockMvcTester.of(new BundleController());
@Test
void getBundle() {
assertThat(mockMvc.perform(get("/bundle")))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
.bodyJson()
.hasPathSatisfying("$.books[0].title",
title -> assertThat(title).isEqualTo("Effective Java"));
}
}
The bridge approach:
- Replaces
MockMvc
withMockMvcTester
- Keeps existing
MockMvcRequestBuilders.get()
calls - Switches from
andExpect()
to AssertJ'sassertThat()
- Allows gradual migration of request building code
- Removes the need for
throws Exception
This is a good intermediate step when migrating large test suites, where you've already invested in custom request builders or utilities based on MockMvcRequestBuilders
.
JSON path assertions
Testing JSON responses becomes more expressive with AssertJ integration.
- Before
- After
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@Test
void verifyJsonContent() throws Exception {
mockMvc.perform(get("/bundle"))
.andExpectAll(
jsonPath("$.books[0].title").value("Effective Java"),
jsonPath("$.books[0].author").value("Joshua Bloch"),
jsonPath("$.books[0].year").value(2001),
jsonPath("$.books").isArray(),
jsonPath("$.books.length()").value(3));
}
Hamcrest-style JSON path assertions:
- Each path requires a separate
jsonPath()
call - Mixing different matcher types (
value()
,isArray()
) - Less fluent and harder to chain
- Limited type safety
import static org.assertj.core.api.Assertions.assertThat;
@Test
void verifyJsonContent() {
assertThat(mockMvc.get().uri("/bundle"))
.hasStatusOk()
.bodyJson()
.hasPathSatisfying("$.books[0].title", title -> assertThat(title).isEqualTo("Effective Java"))
.hasPathSatisfying("$.books[0].author", author -> assertThat(author).isEqualTo("Joshua Bloch"))
.hasPathSatisfying("$.books[0].year", year -> assertThat(year).isEqualTo(2001))
.hasPathSatisfying("$.books", books -> assertThat(books).asList().hasSize(3));
}
AssertJ's JSON path assertions:
- Use familiar AssertJ assertions within
hasPathSatisfying()
- Chain multiple path assertions fluently
- Leverage full AssertJ API for each extracted value
- Better type handling with
asList()
,asMap()
, etc.
Status code assertions
Status assertions are more semantic and expressive with MockMvcTester
.
- Before
- After
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void statusCodes() throws Exception {
mockMvc.perform(get("/ok")).andExpect(status().isOk());
mockMvc.perform(get("/created")).andExpect(status().isCreated());
mockMvc.perform(get("/not-found")).andExpect(status().isNotFound());
mockMvc.perform(get("/error")).andExpect(status().isInternalServerError());
}
Traditional status matchers work but require the status()
wrapper.
import static org.assertj.core.api.Assertions.assertThat;
@Test
void statusCodes() {
assertThat(mockMvc.get().uri("/ok")).hasStatusOk();
assertThat(mockMvc.get().uri("/created")).hasStatus2xxSuccessful();
assertThat(mockMvc.get().uri("/not-found")).hasStatus4xxClientError();
assertThat(mockMvc.get().uri("/error")).hasStatus5xxServerError();
}
AssertJ status assertions provide:
- Direct methods like
hasStatusOk()
,hasStatus2xxSuccessful()
- Better readability without the
status()
wrapper - Consistent with other AssertJ assertions
- Support for status ranges (2xx, 4xx, 5xx)