Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provides patterns for unit testing REST controllers using MockMvc and @WebMvcTest. Generates controller tests that validates request/response mapping, validation, exception handling, and HTTP status codes. Use when testing web layer endpoints in isolation for API endpoint testing, Spring MVC tests, mock HTTP requests, or controller layer unit tests.
.claude/skills/giuseppe-trisciuoglio-unit-test-controller-layer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 86% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 76% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 56% | 0% |
Provides patterns for unit testing @RestController and @Controller classes using MockMvc. Covers request/response handling, HTTP status codes, request parameter binding, validation, content negotiation, response headers, and exception handling with mocked service dependencies.
Use for: controller tests, API endpoint testing, Spring MVC tests, mock HTTP requests, unit testing web layer endpoints, verifying REST controllers in isolation.
MockMvcBuilders.standaloneSetup(controller) for isolated testing@Mock for all services, @InjectMocks for the controllerRun test → If fails: add .andDo(print()) → Check actual vs expected → Fix assertionxml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
javaimport static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @ExtendWith(MockitoExtension.class) class UserControllerTest { @Mock private UserService userService; @InjectMocks private UserController userController; private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); } @Test void shouldReturnAllUsers() throws Exception { List<UserDto> users = List.of(new UserDto(1L, "Alice"), new UserDto(2L, "Bob")); when(userService.getAllUsers()).thenReturn(users); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].id").value(1)) .andExpect(jsonPath("$[0].name").value("Alice")); verify(userService, times(1)).getAllUsers(); } @Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.getUserById(999L)) .thenThrow(new UserNotFoundException("User not found")); mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()); verify(userService).getUserById(999L); } }
java@Test void shouldCreateUserAndReturn201() throws Exception { UserDto createdUser = new UserDto(1L, "Alice", "alice@example.com"); when(userService.createUser(any())).thenReturn(createdUser); mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"name\":\"Alice\",\"email\":\"alice@example.com\"}")) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("Alice")); verify(userService).createUser(any(UserCreateRequest.class)); }
java@Test void shouldUpdateUserAndReturn200() throws Exception { UserDto updatedUser = new UserDto(1L, "Updated"); when(userService.updateUser(eq(1L), any())).thenReturn(updatedUser); mockMvc.perform(put("/api/users/1") .contentType("application/json") .content("{\"name\":\"Updated\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("Updated")); verify(userService).updateUser(eq(1L), any()); }
java@Test void shouldDeleteUserAndReturn204() throws Exception { doNothing().when(userService).deleteUser(1L); mockMvc.perform(delete("/api/users/1")) .andExpect(status().isNoContent()); verify(userService).deleteUser(1L); }
java@Test void shouldFilterUsersByName() throws Exception { when(userService.searchUsers("Alice")).thenReturn(List.of(new UserDto(1L, "Alice"))); mockMvc.perform(get("/api/users/search").param("name", "Alice")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].name").value("Alice")); verify(userService).searchUsers("Alice"); }
java@Test void shouldGetUserByIdFromPath() throws Exception { when(userService.getUserById(123L)).thenReturn(new UserDto(123L, "Alice")); mockMvc.perform(get("/api/users/{id}", 123L)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(123)); }
java@Test void shouldReturn400WhenRequestBodyInvalid() throws Exception { mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"name\":\"\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors").isArray()); }
java@Test void shouldReturnCustomHeaders() throws Exception { when(userService.getAllUsers()).thenReturn(List.of()); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(header().exists("X-Total-Count")) .andExpect(header().string("X-Total-Count", "0")); }
java@Test void shouldRequireAuthorizationHeader() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isUnauthorized()); mockMvc.perform(get("/api/users").header("Authorization", "Bearer token")) .andExpect(status().isOk()); }
java@Test void shouldReturnJsonWhenAcceptHeaderIsJson() throws Exception { when(userService.getUserById(1L)).thenReturn(new UserDto(1L, "Alice")); mockMvc.perform(get("/api/users/1").accept("application/json")) .andExpect(status().isOk()) .andExpect(content().contentType("application/json")); }
standaloneSetup() for isolated controller testingverify(service).method(args)jsonPath() for fluent JSON assertionsstandaloneSetup() may not support @Validated without full context@PreAuthorize/@Secured need additional setup — consider separate security testsMockMultipartFile| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 10,972 | 11,159 | +2% | 1 | 1 | 0% | 2,239 | 4,380 | +96% | 0 | 0 | — |
case-02 | fail→pass | 17,351 | 10,065 | -42% | 1 | 1 | 0% | 3,525 | 4,409 | +25% | 0 | 0 | — |
case-03 | pass→pass | 11,368 | 23,919 | +110% | 1 | 1 | 0% | 2,068 | 3,846 | +86% | 0 | 0 | — |
case-04 | pass→pass | 8,831 | 7,034 | -20% | 1 | 1 | 0% | 1,895 | 3,332 | +76% | 0 | 0 | — |
case-05 | pass→pass | 9,459 | 5,649 | -40% | 1 | 1 | 0% | 1,990 | 3,103 | +56% | 0 | 0 | — |
case-06 | fail→fail | 11,158 | 7,669 | -31% | 1 | 1 | 0% | 2,299 | 3,660 | +59% | 0 | 0 | — |
case-07 | pass→pass | 12,909 | 7,274 | -44% | 1 | 1 | 0% | 2,691 | 3,394 | +26% | 0 | 0 | — |
case-08 | pass→pass | 7,097 | 5,468 | -23% | 1 | 1 | 0% | 1,486 | 3,036 | +104% | 0 | 0 | — |
case-09 | pass→pass | 7,898 | 7,062 | -11% | 1 | 1 | 0% | 1,712 | 3,467 | +103% | 0 | 0 | — |
case-10 | pass→pass | 10,940 | 7,925 | -28% | 1 | 1 | 0% | 2,287 | 3,628 | +59% | 0 | 0 | — |
case-16 | pass→pass | 18,179 | 11,569 | -36% | 1 | 1 | 0% | 2,391 | 3,710 | +55% | 0 | 0 | — |
case-11 | fail→fail | 9,618 | 7,073 | -26% | 1 | 1 | 0% | 1,858 | 3,352 | +80% | 0 | 0 | — |
case-12 | pass→pass | 8,428 | 5,190 | -38% | 1 | 1 | 0% | 1,695 | 2,976 | +76% | 0 | 0 | — |
case-13 | pass→pass | 11,164 | 5,200 | -53% | 1 | 1 | 0% | 2,118 | 2,828 | +34% | 0 | 0 | — |
case-14 | pass→pass | 4,000 | 3,488 | -13% | 1 | 1 | 0% | 665 | 2,522 | +279% | 0 | 0 | — |
case-15 | pass→pass | 13,107 | 8,329 | -36% | 1 | 1 | 0% | 2,382 | 3,149 | +32% | 0 | 0 | — |
case-17 | pass→pass | 6,075 | 5,228 | -14% | 1 | 1 | 0% | 1,194 | 3,043 | +155% | 0 | 0 | — |
case-18 | pass→pass | 10,274 | 8,014 | -22% | 1 | 1 | 0% | 1,677 | 3,367 | +101% | 0 | 0 | — |
case-19 | pass→pass | 3,165 | 2,857 | -10% | 1 | 1 | 0% | 535 | 2,418 | +352% | 0 | 0 | — |
case-20 | pass→pass | 9,032 | 8,965 | -1% | 1 | 1 | 0% | 1,807 | 3,652 | +102% | 0 | 0 | — |
case-21 | pass→pass | 8,856 | 7,232 | -18% | 1 | 1 | 0% | 1,734 | 3,215 | +85% | 0 | 0 | — |
case-22 | pass→pass | 13,661 | 9,477 | -31% | 1 | 1 | 0% | 2,107 | 3,617 | +72% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.