Developer-kit unit-test-exception-handler
Provides patterns for unit testing `@ExceptionHandler` and `@ControllerAdvice` in Spring Boot applications. Validates error response formatting, mocks exceptions, verifies HTTP status codes, tests field-level validation errors, and asserts custom error payloads. Use when writing Spring exception handler tests, REST API error tests, or mocking controller advice.
install
source · Clone the upstream repo
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-java/skills/unit-test-exception-handler" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-unit-test-exception-handler && rm -rf "$T"
manifest:
plugins/developer-kit-java/skills/unit-test-exception-handler/SKILL.mdsource content
Unit Testing ExceptionHandler and ControllerAdvice
Overview
This skill provides patterns for writing unit tests for Spring Boot exception handlers. It covers testing
@ExceptionHandler methods in @ControllerAdvice classes using MockMvc, including HTTP status assertions, JSON response validation, field-level validation error testing, and mocking handler dependencies.
When to Use
- Writing unit tests for
methods@ExceptionHandler - Testing
global exception handling@ControllerAdvice - Validating REST API error response formatting
- Mocking exceptions in controller tests
- Testing field-level validation error responses
- Asserting custom error payloads and HTTP status codes
Instructions
- Create a test controller that throws specific exceptions to trigger each
@ExceptionHandler - Register ControllerAdvice via
onsetControllerAdvice()MockMvcBuilders.standaloneSetup() - Assert HTTP status codes with
.andExpect(status().isXxx()) - Verify error response fields using
matchersjsonPath("$.field") - Test validation errors by sending invalid payloads and checking
produces field-level detailsMethodArgumentNotValidException - Debug failures with
— if handler not invoked, verify.andDo(print())
is called and exception type matchessetControllerAdvice()
Examples
Exception Handler and Error DTO
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse(404, "Not Found", ex.getMessage()); } @ExceptionHandler(ValidationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidation(ValidationException ex) { return new ErrorResponse(400, "Bad Request", ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ValidationErrorResponse handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage())); return new ValidationErrorResponse(400, "Validation Failed", errors); } } public record ErrorResponse(int status, String error, String message) {} public record ValidationErrorResponse(int status, String error, Map<String, String> errors) {}
Unit Test
@ExtendWith(MockitoExtension.class) class GlobalExceptionHandlerTest { private MockMvc mockMvc; @BeforeEach void setUp() { GlobalExceptionHandler handler = new GlobalExceptionHandler(); mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) .setControllerAdvice(handler) .build(); } @Test void shouldReturn404WhenResourceNotFound() throws Exception { mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value(404)) .andExpect(jsonPath("$.error").value("Not Found")) .andExpect(jsonPath("$.message").value("User not found")); } @Test void shouldReturn400WithFieldErrorsOnValidationFailure() throws Exception { mockMvc.perform(post("/api/users") .contentType("application/json") .content("{\"name\":\"\",\"email\":\"invalid\"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.status").value(400)) .andExpect(jsonPath("$.errors.name").value("must not be blank")) .andExpect(jsonPath("$.errors.email").value("must be a valid email")); } } @RestController @RequestMapping("/api") class TestController { @GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { throw new ResourceNotFoundException("User not found"); } @PostMapping("/users") public User createUser(@RequestBody @Valid User user) { throw new ValidationException("Validation failed"); } }
Best Practices
- Test each
method independently with a dedicated exception throw@ExceptionHandler - Register exactly one
instance via@ControllerAdvice
— never skip itsetControllerAdvice() - Assert all fields in the error response body, not just the HTTP status
- For validation errors, verify both the field name key and the error message value
- Use
for isolated handler tests without full Spring contextMockMvcBuilders.standaloneSetup() - Log assertion failures: chain
to print request/response when a test fails.andDo(print())
Common Pitfalls
- Handler not invoked: ensure
is called on the buildersetControllerAdvice() - JsonPath mismatch: use
to inspect actual response structure.andDo(print()) - Status is 200: missing
on the handler method@ResponseStatus - Duplicate handlers:
controls precedence; more specific exception types take priority@Order - Testing handler logic instead of behavior: mock external dependencies, test only the response transformation
Constraints and Warnings
specificity: more specific exception types are matched first;@ExceptionHandler
catches all unmatched typesException.class
default: without@ResponseStatus
or returning@ResponseStatus
, HTTP status defaults to 200ResponseEntity- Global vs local scope:
in@ExceptionHandler
is global; declared in a controller it is local only to that controller@ControllerAdvice - Logging side effects: handlers that log should be verified with
verify(mockLogger).logXxx(...) - Localization: when using
, test with differentMessageSource
values to confirm message resolutionLocale - Security context:
handlers can accessAuthorizationException
— test that context is correctly evaluatedSecurityContextHolder