Testing is a crucial part of developing robust web applications, especially RESTful services built with Spring Boot. This detailed tutorial guides you step by step through the process of unit testing the web, business, and data layers of a Spring Boot application using MockMvc, Mockito, and key Spring testing features. You’ll also learn best practices for test design, response assertions, and integration testing.
Introduction
Real-world applications typically have a layered architecture:
- Web Layer: Exposes REST endpoints
- Business Layer: Contains business logic/services
- Data Layer: Handles persistence (database operations)
To ensure each part works correctly and efficiently, and to catch bugs early, you should write unit tests for each layer and integration tests for combined functionality.
Why Test in Layers?
- Isolated testing allows you to test just one part at a time (e.g., only the web/controller layer), making it easier to find and fix bugs.
- Integration testing ensures multiple layers work together as expected.
- Mocking is used to simulate or replace real dependencies so tests run quickly and deterministically.
Step 1: Creating a Simple REST Controller
Let’s start by creating a basic REST controller in Spring Boot that returns a simple string.
@RestController
public class HelloWorldController {
@GetMapping("/hello-world")
public String helloWorld() {
return "Hello, world";
}
}
Step 2: Writing Unit Tests for the Controller
We want to test only the HelloWorldController, not the entire application. For this, Spring’s MockMvc framework is ideal.
@RunWith(SpringRunner.class)
@WebMvcTest(value = HelloWorldController.class)
public class HelloWorldControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void helloWorld_basic() throws Exception {
mockMvc.perform(get("/hello-world")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string("Hello, world"));
}
}
@RunWith(SpringRunner.class)sets up the Spring test context.@WebMvcTestloads only the specified controller (HelloWorldController). It does not load other beans or service layers.mockMvc.perform(...)simulates an HTTP request without starting a server.andExpectallows you to assert the HTTP status, content, headers, etc.
Step 3: Improving Assertions with MockMvc
Beyond basic string assertions, MockMvc provides powerful capabilities for verifying status codes and response content.
Matching the Status
Add assertion for HTTP Status:
.andExpect(status().isOk())
Step 4: Testing Controllers that Return JSON Objects
Let’s return a more complex object from the controller:
@RestController
public class ItemController {
@GetMapping("/dummy-item")
public Item getDummyItem() {
return new Item(1, "Ball", 10, 100);
}
}
public class Item {
private int id;
private String name;
private int price;
private int quantity;
// Getters and setters omitted for brevity
}
Writing a Unit Test for JSON Response
@RunWith(SpringRunner.class)
@WebMvcTest(ItemController.class)
public class ItemControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void dummyItem_basic() throws Exception {
mockMvc.perform(get("/dummy-item")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json("{\"id\":1,\"name\":\"Ball\",\"price\":10,\"quantity\":100}"));
}
}
Why use content().json instead of content().string?
content().jsonis smarter:- Ignores field order
- Ignores whitespace and formatting
- Allows partial matches (if, for example, you only care about some fields)
content().stringrequires an exact character-for-character match.
For example, if you write:
.andExpect(content().json("{\"id\":1,\"name\":\"Ball\",\"price\":10}"))
The test passes as long as those fields match, even if the actual response contains more fields.
Best Practices and Tips
- Use
@WebMvcTestfor controller (web layer) tests. - Use MockMvc to issue HTTP requests within tests.
- Use
content().jsonfor JSON assertions—more robust and readable than string matching. - Only include additional assertions (
assertEquals) if your scenario requires them. - Keep tests simple and focused: Test one thing per test method.
- For complex business logic or database interaction, mock dependencies using Mockito (not covered in this introductory controller-focused tutorial).
- If you include a business or data layer, write unit tests for those classes separately, and use integration tests to check the complete workflow.
- Don’t be discouraged if tests feel verbose—the clarity and safety are worth it.
