Learn how to test your Spring REST APIs using Spring Boot 3 and Spring Framework 6 testing tools. This guide walks you through five different testing approaches, from simple unit tests to full end-to-end tests.
Spring Boot 3 provides several mature testing tools for REST APIs:
- MockMvc - Test Spring MVC controllers without a running server
- WebTestClient - Reactive/functional style API for testing (works with both reactive and servlet stacks)
- TestRestTemplate - Full HTTP testing with an embedded server
Why use these tools?
- Battle-tested and widely adopted
- Rich feature set for all types of tests
- Excellent documentation and community support
- Works for unit tests, integration tests, and end-to-end tests
Spring Boot 3 offers 5 different ways to test your API. Here's a simple guide:
| Method | Speed | What It Tests | Best For |
|---|---|---|---|
MockMvc (standalone) |
β‘ Fastest | Just your controller | Quick unit tests |
MockMvc (@WebMvcTest) |
π Fast | Controller + Spring MVC | Testing validation, security |
WebTestClient (@SpringBootTest) |
π’ Slower | Full app (no HTTP) | Real database tests, reactive apps |
TestRestTemplate |
π Slowest | Everything with HTTP | Complete end-to-end tests |
WebTestClient (RouterFunction) |
β‘ Fastest | Functional endpoints | WebFlux functional routes |
Not sure? Start with MockMvc standalone for simple tests and @SpringBootTest with WebTestClient for integration tests.
What it does: Tests just your controller logic, nothing else. No Spring, no database, no validation.
When to use: When you want super-fast tests for your controller logic.
public class TodoControllerUnitTest {
MockMvc mockMvc;
TodoService todoService;
@BeforeEach
void setup() {
// Mock your dependencies
todoService = Mockito.mock(TodoService.class);
when(todoService.getAllTodos()).thenReturn(
List.of(new Todo(1L, 1L, "Test Todo", false))
);
// Create standalone MockMvc
mockMvc = MockMvcBuilders
.standaloneSetup(new TodoController(todoService))
.build();
}
@Test
void shouldGetAllTodos() throws Exception {
mockMvc.perform(get("/api/todos"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Test Todo"));
}
}β Pros: Super fast, simple setup β Cons: No validation, security, or other Spring features
What it does: Tests your controller with Spring MVC features like validation, security, and exception handling.
When to use: When you need to test validation rules, security, or @ControllerAdvice error handling.
@WebMvcTest(TodoController.class)
public class TodoControllerMvcTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private TodoService todoService;
@Test
void shouldValidateInput() throws Exception {
// Test validation - empty title should fail
mockMvc.perform(post("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"userId": null, "title": "", "completed": false}
"""))
.andExpect(status().isBadRequest());
}
@Test
@WithMockUser
void shouldAllowAuthenticatedUser() throws Exception {
when(todoService.getAllTodos()).thenReturn(
List.of(new Todo(1L, 1L, "Secured Todo", false))
);
mockMvc.perform(get("/api/todos"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Secured Todo"));
}
}β Pros: Tests validation, security, and error handling β Cons: Services are still mocked, no real database
What it does: Tests your full application with real services and database - but without actual HTTP.
When to use: When you want to test the complete flow including database operations.
@SpringBootTest
@AutoConfigureWebTestClient
@Transactional // Rolls back after each test
public class TodoControllerIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private TodoRepository todoRepository;
@Autowired
private TodoService todoService;
@Test
void shouldCreateAndRetrieveTodo() {
// Create a todo - this hits the real database
webTestClient.post()
.uri("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new Todo(null, 1L, "Integration Test", false))
.exchange()
.expectStatus().isCreated();
// Verify it's in the database
assertEquals(1, todoService.getAllTodos().size());
// Retrieve it
webTestClient.get()
.uri("/api/todos")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].title").isEqualTo("Integration Test");
}
}Alternative: Using MockMvc instead of WebTestClient
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateAndRetrieveTodo() throws Exception {
mockMvc.perform(post("/api/todos")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"userId": 1, "title": "Integration Test", "completed": false}
"""))
.andExpect(status().isCreated());
mockMvc.perform(get("/api/todos"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Integration Test"));
}
}β Pros: Real database, real services, tests actual behavior β Cons: Slower, can't test HTTP-specific features (like CORS)
What it does: Tests everything including real HTTP requests - the most complete testing possible.
When to use: When you need to test HTTP-specific features like CORS, headers, or compression.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerServerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void findAllTodos() {
ResponseEntity<List<Todo>> response = restTemplate.exchange(
"/api/todos",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Todo>>() {}
);
assertEquals(HttpStatus.OK, response.getStatusCode());
List<Todo> todos = response.getBody();
assertEquals(200, todos.size());
assertEquals("delectus aut autem", todos.get(0).title());
assertFalse(todos.get(0).completed());
}
@Test
void createTodo() {
Todo newTodo = new Todo(null, 1L, "New Todo", false);
ResponseEntity<Todo> response = restTemplate.postForEntity(
"/api/todos",
newTodo,
Todo.class
);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertNotNull(response.getBody().id());
}
}Alternative: Using WebTestClient for end-to-end tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerServerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void findAllTodos() {
webTestClient.get()
.uri("/api/todos")
.exchange()
.expectStatus().isOk()
.expectBodyList(Todo.class)
.hasSize(200)
.value(todos -> {
assertEquals("delectus aut autem", todos.get(0).title());
assertFalse(todos.get(0).completed());
});
}
}β Pros: Most realistic, tests everything including HTTP β Cons: Slowest, can be flaky due to network issues
What it does: Tests functional/reactive endpoints (WebFlux style).
When to use: Only if you're using RouterFunction instead of @RestController.
public class TodoRouterTest {
private TodoHandler todoHandler;
private WebTestClient webTestClient;
@BeforeEach
void setup() {
todoHandler = new TodoHandler(todoService);
RouterFunction<ServerResponse> routes = RouterFunctions.route()
.GET("/api/todos", todoHandler::getAllTodos)
.POST("/api/todos", todoHandler::createTodo)
.build();
webTestClient = WebTestClient
.bindToRouterFunction(routes)
.build();
}
@Test
void shouldTestFunctionalEndpoint() {
webTestClient.get()
.uri("/api/todos")
.exchange()
.expectStatus().isOk()
.expectBodyList(Todo.class);
}
}β Pros: Fast, good for reactive apps β Cons: Only works with RouterFunction (not @RestController)
Add these to your pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Optional: For WebTestClient support with servlet stack -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>For Gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Optional: For WebTestClient support
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
}# Run all tests
./mvnw test
# Run a specific test
./mvnw test -Dtest=TodoControllerMvcTest- Many unit tests (standalone MockMvc) - fast feedback
- Some integration tests (@SpringBootTest) - test real behavior
- Few end-to-end tests (TestRestTemplate) - catch edge cases
1. Using @MockBean without Spring
// β Won't work
public class Test {
@MockBean TodoService service;
}
// β
Works
@WebMvcTest
public class Test {
@MockBean TodoService service;
}2. Testing HTTP features without a server
// β CORS won't work here
@WebMvcTest
// β
Use this for CORS/headers
@SpringBootTest(webEnvironment = RANDOM_PORT)3. Forgetting to add WebFlux dependency for WebTestClient
<!-- Need this in test scope to use WebTestClient with servlet stack -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>| Feature | MockMvc | WebTestClient | TestRestTemplate |
|---|---|---|---|
| Style | Traditional | Fluent/Reactive | Traditional |
| HTTP Server | No | No (usually) | Yes |
| Performance | Fast | Fast | Slower |
| Reactive Support | No | Yes | No |
| Best Use Case | MVC testing | Modern/reactive apps | E2E testing |
When you're ready to upgrade to Spring Framework 7 (Spring Boot 4), the migration is straightforward:
// Spring Boot 3
mockMvc.perform(get("/api/todos"))
.andExpect(status().isOk());
// Spring Boot 4 / Framework 7
RestTestClient.bindToMockMvc(mockMvc)
.get().uri("/api/todos")
.exchange()
.expectStatus().isOk();// Spring Boot 3
webTestClient.get()
.uri("/api/todos")
.exchange()
.expectStatus().isOk();
// Spring Boot 4 / Framework 7 - Very similar!
RestTestClient.bindToApplicationContext(context)
.get().uri("/api/todos")
.exchange()
.expectStatus().isOk();// Spring Boot 3
testRestTemplate.getForEntity("/api/todos", Todo[].class);
// Spring Boot 4 / Framework 7
RestTestClient.bindToServer()
.get().uri("/api/todos")
.exchange()
.expectStatus().isOk();- Spring Boot 3 Testing Documentation
- Spring Framework 6 Testing Guide
- MockMvc Documentation
- WebTestClient Documentation
Adapted for Spring Boot 3 / Spring Framework 6 Original guide by Dan Vega