Testing Exception Handling in Spring Services
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
To test exception handling in a service, you stub the mock dependency to throw an exception using when-thenThrow.
For example, if you're testing a service that calls a repository, you'd mock the repository and make findById throw a DataAccessException to simulate a database failure. Then you use assertThrows from JUnit 5 to verify your service handles it correctly. assertThrows takes the expected exception class and a lambda that calls your service method.
If that exception is thrown, the test passes and assertThrows returns the exception so you can make additional assertions on it. There are two patterns you typically test. The first is propagation, where your service just lets the exception bubble up unchanged.
You'd verify that the same exception type escapes the service. The second is translation, where your service catches the low-level exception and wraps it in a custom domain exception with more context.
For example, catching DataAccessException and throwing OrderNotFoundException with the specific order ID that failed. In that case, you'd assert on the custom exception type, check that the message includes the order ID, and verify that the original exception is preserved as the cause using getCause. The common mistake freshers make is only testing the happy path—when everything works—and leaving all the catch blocks untested.
That tanks your code coverage and means your error handling code has never actually run until it fails in production.
Testing Exception Handling in Spring Services
Direct Answer
Stub the mock to throw the exception using when().thenThrow(), then use assertThrows() from JUnit 5 to verify that your service throws the expected exception or handles it correctly. Test both propagation—service re-throws the exception—and translation—service wraps the exception in a domain-specific exception.
Code Example
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
private OrderService orderService;
@BeforeEach
void setUp() {
orderService = new OrderService(orderRepository);
}
@Test
void findOrder_throwsOrderNotFoundWhenRepositoryFails() {
Long orderId = 999L;
// Stub repository to throw exception
when(orderRepository.findById(orderId))
.thenThrow(new DataAccessException("Database connection failed") {});
var exception = assertThrows(OrderNotFoundException.class,
() -> orderService.findOrder(orderId));
assertEquals("Order 999 not found", exception.getMessage());
assertInstanceOf(DataAccessException.class, exception.getCause()); // verify wrapped cause
}
}
Common Mistakes & Pitfalls
- Only testing the happy path: Freshers write tests where everything succeeds—repository returns data, external API responds—but never test what happens when dependencies throw exceptions, leaving error handling code with zero test coverage.
- Using try-catch in tests: Developers write try-catch blocks in test methods to capture exceptions, making tests verbose and easy to get wrong—JUnit 5's assertThrows is cleaner and returns the exception for further assertions.
- Not verifying the exception message or cause: Tests assert that the right exception type is thrown but don't check the message or wrapped cause, missing bugs where error context is lost or the wrong exception is wrapped.
Check your understanding
Answer these without looking back at the article.
- How do you assert that a specific exception message is returned?
- How do you test that your service logs the exception without exposing it to the caller?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.