java
59 lines · 8 steps
Slice testing a controller with @WebMvcTest in Spring
A focused web-layer test that loads only the controller, mocks the service, and asserts HTTP behavior end to end.
Explained by
highlit
1@WebMvcTest(OrderController.class)
2class OrderControllerTest {
3
4 @Autowired
5 private MockMvc mockMvc;
6
7 @Autowired
8 private ObjectMapper objectMapper;
9
10 @MockBean
11 private OrderService orderService;
12
13 @Test
14 void returnsOrderWhenFound() throws Exception {
15 var order = new OrderResponse(42L, "SHIPPED", new BigDecimal("129.99"));
16 when(orderService.findById(42L)).thenReturn(order);
17
18 mockMvc.perform(get("/api/orders/{id}", 42L))
19 .andExpect(status().isOk())
20 .andExpect(jsonPath("$.id").value(42))
21 .andExpect(jsonPath("$.status").value("SHIPPED"))
22 .andExpect(jsonPath("$.total").value(129.99));
23 }
24
25 @Test
26 void returns404WhenMissing() throws Exception {
27 when(orderService.findById(99L)).thenThrow(new OrderNotFoundException(99L));
28
29 mockMvc.perform(get("/api/orders/{id}", 99L))
30 .andExpect(status().isNotFound())
31 .andExpect(jsonPath("$.message").value("Order 99 not found"));
32 }
33
34 @Test
35 void createsOrderFromValidPayload() throws Exception {
36 var request = new CreateOrderRequest("ACME-7", 3);
37 when(orderService.create(any(CreateOrderRequest.class)))
38 .thenReturn(new OrderResponse(7L, "PENDING", new BigDecimal("59.97")));
39
40 mockMvc.perform(post("/api/orders")
41 .contentType(MediaType.APPLICATION_JSON)
42 .content(objectMapper.writeValueAsString(request)))
43 .andExpect(status().isCreated())
44 .andExpect(header().string("Location", "/api/orders/7"))
45 .andExpect(jsonPath("$.status").value("PENDING"));
46 }
47
48 @Test
49 void rejectsInvalidPayload() throws Exception {
50 var request = new CreateOrderRequest("", 0);
51
52 mockMvc.perform(post("/api/orders")
53 .contentType(MediaType.APPLICATION_JSON)
54 .content(objectMapper.writeValueAsString(request)))
55 .andExpect(status().isBadRequest());
56
57 verifyNoInteractions(orderService);
58 }
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1@WebMvcTest loads just the web layer so tests stay fast and focused on request handling.
- 2Mocking the service with @MockBean lets each test pin down exactly what the controller receives.
- 3MockMvc lets you assert status codes, headers, and JSON fields without a running server.
Related explainers
java
public interface GitHubClient { @GetExchange("/users/{username}") GitHubUser getUser(@PathVariable String username);
Declarative HTTP clients in Spring
http-client
declarative-api
proxy
Intermediate
8 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
java
@RestController @RequestMapping("/api/products") public class ProductSearchController {
Binding collection query params in Spring
rest-api
query-parameters
dependency-injection
Intermediate
6 steps
java
import java.util.ArrayDeque; import java.util.Deque; import java.util.Map;
Evaluating math expressions with two stacks
stacks
parsing
operator-precedence
Intermediate
9 steps
go
func UploadChunk(c *gin.Context) { uploadID := c.Param("uploadID") if !validUploadID.MatchString(uploadID) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload id"})
Resumable chunked uploads in Gin
file-upload
content-range
streaming
Advanced
9 steps
ruby
require "timeout" require "net/http" class RemoteInventoryClient
Bounding a slow HTTP call with Timeout in Ruby
timeout
error-handling
http
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/slice-testing-a-controller-with-webmvctest-in-spring-explained-java-bcf0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.