unit-test-controller-layer
Install this skill
npx skills add giuseppe-trisciuoglio/developer-kitWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
This skill focuses on validating Spring Boot REST controller logic in isolation by simulating HTTP interactions. By using MockMvc, developers can verify how endpoints process incoming requests, handle service-layer data, and format outgoing JSON payloads without triggering the full Spring ApplicationContext. This approach ensures rapid feedback loops for verifying HTTP status codes, request parameter bindings, and JSON path assertions. The workflow involves mocking service-level dependencies with Mockito to ensure the controller remains the sole focus of the test, effectively decoupling the web layer from database connectivity or business logic overhead. It is a critical practice for maintaining contract integrity between the API interface and the underlying service implementation while keeping the test suite runtime exceptionally low.
When to Use This Skill
- •Confirming that controller endpoints correctly map URI paths to service methods
- •Validating incoming request body serialization and deserialization
- •Testing exception handling logic that returns specific HTTP error codes
- •Verifying that request parameters or path variables are bound correctly
- •Ensuring APIs return expected JSON formats based on service responses
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “Write a unit test for my Spring REST controller
- “How do I use MockMvc to verify my controller response?
- “Test my POST endpoint with Mockito
- “Asserting JSON path in MockMvc tests
- “Create a test class for this RestController
- “How to mock service dependencies in controller tests
Pro Tips
- 💡Leverage `@WebMvcTest` to load only web-related components of the Spring context, ensuring fast and isolated controller tests.
- 💡Always mock service layer dependencies using `@MockBean` or Mockito to prevent integration with business logic and focus solely on the controller's behavior.
- 💡Utilize `MockMvcResultMatchers` extensively for comprehensive assertions on HTTP status, response content (JSONPath, String), headers, and cookies.
What this skill does
- •Simulating HTTP GET, POST, PUT, and DELETE requests
- •Asserting HTTP response status codes such as 200, 201, 400, and 404
- •Validating response body structures using JSON path expressions
- •Mocking service dependencies to isolate controller logic
- •Verifying content negotiation and request header processing
When not to use it
- ✕When you need to test actual database interactions or real service integrations
- ✕When verifying how the full Spring security filter chain interacts with real authentication providers
- ✕When end-to-end flow validation across multiple microservices is required
Example workflow
- Initialize the test class with @ExtendWith(MockitoExtension.class)
- Define a mock for the service layer and inject it into the controller
- Configure MockMvc using MockMvcBuilders.standaloneSetup
- Define test behavior for the mocked service using Mockito's when/then statements
- Execute the HTTP request using mockMvc.perform and verify the response status and content
- Perform verification of the service call interaction
Prerequisites
- –Spring Boot project structure
- –JUnit 5 and Mockito dependencies
- –Basic knowledge of Jackson or JSON structure
Pitfalls & limitations
- !Over-testing business logic inside the controller instead of the service layer
- !Forgetting to verify service methods after asserting the controller response
- !Inconsistent JSON path expressions failing due to minor whitespace or formatting changes
- !Failure to handle custom exception serialization in error scenarios
FAQ
How it compares
Unlike manual testing with Postman or generic integration tests that boot the entire server, this skill allows for automated, millisecond-speed validation that remains stable even if external database configurations change.
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
Test @RestController and @Controller classes by mocking service dependencies and verifying HTTP responses, status codes, and serialization. Use MockMvc for lightweight controller testing without loading the full Spring context.
## When to Use This Skill
Use this skill when:
- Testing REST controller request/response handling
- Verifying HTTP status codes and response formats
- Testing request parameter binding and validation
- Mocking service layer for isolated controller tests
- Testing content negotiation and response headers
- Want fast controller tests without integration test overhead
## Setup: MockMvc + Mockito
### Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>### Gradle
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.mockito:mockito-core")
}## Basic Pattern: Testing GET Endpoint
### Simple GET Endpoint Test
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import 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;
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("$").isArray())
.andExpect(jsonPath("$[0].id").value(1))
.andExpect(jsonPath("$[0].name").value("Alice"))
.andExpect(jsonPath("$[1].id").value(2));
verify(userService, times(1)).getAllUsers();
}
@Test
void shouldReturnUserById() throws Exception {
UserDto user = new UserDto(1L, "Alice");
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Alice"));
verify(userService).getUserById(1L);
}
}## Testing POST Endpoint
### Create Resource with Request Body
@Test
void shouldCreateUserAndReturn201() throws Exception {
UserCreateRequest request = new UserCreateRequest("Alice", "[email protected]");
UserDto createdUser = new UserDto(1L, "Alice", "[email protected]");
when(userService.createUser(any(UserCreateRequest.class)))
.thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content("{\"name\":\"Alice\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("[email protected]"));
verify(userService).createUser(any(UserCreateRequest.class));
}## Testing Error Scenarios
### Handle 404 Not Found
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.getUserById(999L))
.thenThrow(new UserNotFoundException("User not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("User not found"));
verify(userService).getUserById(999L);
}### Handle 400 Bad Request
@Test
void shouldReturn400WhenRequestBodyInvalid() throws Exception {
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content("{\"name\":\"\"}")) // Empty name
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray());
}## Testing PUT/PATCH Endpoints
### Update Resource
@Test
void shouldUpdateUserAndReturn200() throws Exception {
UserUpdateRequest request = new UserUpdateRequest("Alice Updated");
UserDto updatedUser = new UserDto(1L, "Alice Updated");
when(userService.updateUser(eq(1L), any(UserUpdateRequest.class)))
.thenReturn(updatedUser);
mockMvc.perform(put("/api/users/1")
.contentType("application/json")
.content("{\"name\":\"Alice Updated\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Alice Updated"));
verify(userService).updateUser(eq(1L), any(UserUpdateRequest.class));
}## Testing DELETE Endpoint
### Delete Resource
@Test
void shouldDeleteUserAndReturn204() throws Exception {
doNothing().when(userService).deleteUser(1L);
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());
verify(userService).deleteUser(1L);
}
@Test
void shouldReturn404WhenDeletingNonExistentUser() throws Exception {
doThrow(new UserNotFoundException("User not found"))
.when(userService).deleteUser(999L);
mockMvc.perform(delete("/api/users/999"))
.andExpect(status().isNotFound());
}## Testing Request Parameters
### Query Parameters
@Test
void shouldFilterUsersByName() throws Exception {
List<UserDto> users = List.of(new UserDto(1L, "Alice"));
when(userService.searchUsers("Alice")).thenReturn(users);
mockMvc.perform(get("/api/users/search?name=Alice"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].name").value("Alice"));
verify(userService).searchUsers("Alice");
}### Path Variables
@Test
void shouldGetUserByIdFromPath() throws Exception {
UserDto user = new UserDto(123L, "Alice");
when(userService.getUserById(123L)).thenReturn(user);
mockMvc.perform(get("/api/users/{id}", 123L))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(123));
}## Testing Response Headers
### Verify Response Headers
@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"))
.andExpect(header().string("Content-Type", containsString("application/json")));
}## Testing Request Headers
### Send Request Headers
@Test
void shouldRequireAuthorizationHeader() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
mockMvc.perform(get("/api/users")
.header("Authorization", "Bearer token123"))
.andExpect(status().isOk());
}## Content Negotiation
### Test Different Accept Headers
@Test
void shouldReturnJsonWhenAcceptHeaderIsJson() throws Exception {
UserDto user = new UserDto(1L, "Alice");
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1")
.accept("application/json"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"));
}## Advanced: Testing Multiple Status Codes
@Test
void shouldReturnDifferentStatusCodesForDifferentScenarios() throws Exception {
// Successful response
when(userService.getUserById(1L)).thenReturn(new UserDto(1L, "Alice"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
// Not found
when(userService.getUserById(999L))
.thenThrow(new UserNotFoundException("Not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
// Unauthorized
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isUnauthorized());
}## Best Practices
- **Use standalone setup** when testing single controller:
MockMvcBuilders.standaloneSetup()- **Mock service layer** - controllers should focus on HTTP handling
- **Test happy path and error paths** thoroughly
- **Verify service method calls** to ensure controller delegates correctly
- **Use content() matchers** for response body validation
- **Keep tests focused** on one endpoint behavior per test
- **Use JsonPath** for fluent JSON response assertions
## Common Pitfalls
- **Testing business logic in controller**: Move to service tests
- **Not mocking service layer**: Always mock service dependencies
- **Testing framework behavior**: Focus on your code, not Spring code
- **Hardcoding URLs**: Use MockMvcRequestBuilders helpers
- **Not verifying mock interactions**: Always verify service was called correctly
## Troubleshooting
**Content type mismatch**: Ensure
contentType() matches controller's @PostMapping(consumes=...) or use default.**JsonPath not matching**: Use
mockMvc.perform(...).andDo(print()) to see actual response content.**Status code assertions fail**: Check controller
@RequestMapping, @PostMapping status codes and error handling.## References
- [Spring MockMvc Documentation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/MockMvc.html)
- [JsonPath for REST Assertions](https://goessner.net/articles/JsonPath/)
- [Spring Testing Best Practices](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/unit-test-controller-layer/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-controller-layer/SKILL.md - Cursor:
~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-controller-layer/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-controller-layer/SKILL.md
🚀 Install with CLI:npx skills add giuseppe-trisciuoglio/developer-kit