Spring Boot Testing
1. Add testing dependencies
spring-boot-starter-test includes JUnit 5, Mockito,
AssertJ, Spring Test, and tools for MVC and JSON testing.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2. Choose the right test level
| Test type | What it loads | Typical annotation | Speed |
|---|---|---|---|
| Unit | One class and mocked dependencies | @ExtendWith(MockitoExtension.class) |
Very fast |
| Web slice | Controller, MVC, JSON, filters | @WebMvcTest |
Fast |
| Data slice | JPA repositories and database test setup | @DataJpaTest |
Medium |
| Integration | Complete Spring application context | @SpringBootTest |
Slower |
3. Example domain: Employee and Department
We will test an Employee service that reads an employee from a repository and looks up the employee's Department. The service should throw a clear exception when the employee or department does not exist.
public record Employee(
Long id, String name, Long departmentId) {
}
public record Department(Long id, String name) {
}
public record EmployeeDetails(
Long employeeId,
String employeeName,
String departmentName) {
}
4. Employee service to test
@Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
private final DepartmentRepository departmentRepository;
public EmployeeService(
EmployeeRepository employeeRepository,
DepartmentRepository departmentRepository) {
this.employeeRepository = employeeRepository;
this.departmentRepository = departmentRepository;
}
public EmployeeDetails findDetails(Long employeeId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new EmployeeNotFoundException(employeeId));
Department department = departmentRepository
.findById(employee.departmentId())
.orElseThrow(() -> new DepartmentNotFoundException(
employee.departmentId()));
return new EmployeeDetails(
employee.id(), employee.name(), department.name());
}
}
5. Important Mockito annotations
| Annotation | Purpose |
|---|---|
@ExtendWith(MockitoExtension.class) |
Starts Mockito and initializes annotations for a JUnit 5 test. |
@Mock |
Creates a fake dependency whose methods return default values until stubbed. |
@InjectMocks |
Creates the class under test and injects its mocks. |
@Spy |
Wraps a real object; real methods run unless selectively stubbed. |
@Captor |
Creates an ArgumentCaptor for inspecting values passed to a mock. |
@MockBean |
Replaces a bean in a Spring test context; common in older Spring Boot versions. |
@MockitoBean |
Spring's newer bean-mocking annotation for test application contexts. |
@MockitoSpyBean |
Spring's newer spy equivalent for a bean in the test context. |
6. Basic Mockito unit test
The test below does not start Spring and does not connect to a database. Mockito supplies fake repositories, so the test focuses only on the EmployeeService behavior.
@ExtendWith(MockitoExtension.class)
class EmployeeServiceTest {
@Mock
private EmployeeRepository employeeRepository;
@Mock
private DepartmentRepository departmentRepository;
@InjectMocks
private EmployeeService employeeService;
@Test
void returnsEmployeeWithDepartmentName() {
Employee employee = new Employee(1L, "Anand", 10L);
Department department = new Department(10L, "Engineering");
when(employeeRepository.findById(1L))
.thenReturn(Optional.of(employee));
when(departmentRepository.findById(10L))
.thenReturn(Optional.of(department));
EmployeeDetails result = employeeService.findDetails(1L);
assertThat(result.employeeName()).isEqualTo("Anand");
assertThat(result.departmentName()).isEqualTo("Engineering");
}
}
7. Stubbing mock behavior
Stubbing tells Mockito what a dependency should return or throw when a
method is called. Use when(...).thenReturn(...) for normal
methods and thenThrow for failure scenarios.
when(employeeRepository.findById(1L))
.thenReturn(Optional.of(employee));
when(employeeRepository.findById(99L))
.thenReturn(Optional.empty());
when(employeeRepository.count())
.thenReturn(25L);
when(employeeRepository.save(any(Employee.class)))
.thenThrow(new DataIntegrityViolationException("Duplicate email"));
For void methods use doNothing(), doThrow(),
or doAnswer().
doNothing().when(emailService).sendWelcomeEmail(anyString());
doThrow(new MailServerException()).when(emailService)
.sendWelcomeEmail("failed@example.com");
8. Verify interactions
Assertions check the returned result. Mockito verification checks how the service used its dependencies. Verify important behavior, not every internal implementation detail.
verify(employeeRepository).findById(1L);
verify(departmentRepository).findById(10L);
verify(employeeRepository, times(1)).findById(1L);
verify(emailService, never()).sendWelcomeEmail(anyString());
verify(departmentRepository, atLeastOnce()).findById(anyLong());
verifyNoMoreInteractions(employeeRepository);
9. Test missing employee and department
@Test
void throwsWhenEmployeeDoesNotExist() {
when(employeeRepository.findById(99L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> employeeService.findDetails(99L))
.isInstanceOf(EmployeeNotFoundException.class)
.hasMessage("Employee 99 was not found");
verify(employeeRepository).findById(99L);
verifyNoInteractions(departmentRepository);
}
@Test
void throwsWhenDepartmentDoesNotExist() {
when(employeeRepository.findById(1L))
.thenReturn(Optional.of(new Employee(1L, "Anand", 50L)));
when(departmentRepository.findById(50L))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> employeeService.findDetails(1L))
.isInstanceOf(DepartmentNotFoundException.class);
}
10. Argument matchers
Matchers let a stub or verification work with a range of values. Do not
mix a raw value and a matcher in the same method call; use
eq for the raw value.
when(employeeRepository.findByDepartmentId(
eq(10L), any(Pageable.class)))
.thenReturn(List.of(employee));
verify(employeeRepository).findById(eq(1L));
verify(employeeRepository).findById(anyLong());
verify(employeeRepository).save(argThat(e ->
e.name().startsWith("Anand")));
// Correct: all arguments use matchers
when(client.fetch(eq("employees"), anyInt()))
.thenReturn(response);
11. Capture method arguments
ArgumentCaptor is useful when you want to inspect the
object sent to a repository or email client.
@Captor
private ArgumentCaptor<Employee> employeeCaptor;
@Test
void savesEmployeeWithExpectedValues() {
when(employeeRepository.save(any(Employee.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
employeeService.create("Anand", 10L);
verify(employeeRepository).save(employeeCaptor.capture());
Employee saved = employeeCaptor.getValue();
assertThat(saved.name()).isEqualTo("Anand");
assertThat(saved.departmentId()).isEqualTo(10L);
}
12. @Spy and InOrder
A spy uses a real object and is useful when most behavior should remain
real but one method needs to be replaced. InOrder checks a
sequence when call order is part of the business behavior.
@Spy
private EmployeeMapper mapper = new EmployeeMapper();
doReturn("test-name").when(mapper).normalize(anyString());
InOrder order = inOrder(employeeRepository, departmentRepository);
order.verify(employeeRepository).findById(1L);
order.verify(departmentRepository).findById(10L);
13. Mockito settings and lifecycle
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
class DepartmentServiceTest {
@BeforeEach
void setUp() {
// Prepare common test data only when it helps readability.
}
@AfterEach
void tearDown() {
// Release external resources created by the test.
}
}
Strict stubbing helps identify unused or incorrect stubs. Prefer a new
test fixture for each test rather than calling reset(mock),
which can hide a test's intent.
14. Department service example
@Service
public class DepartmentService {
private final DepartmentRepository repository;
private final EmployeeRepository employeeRepository;
public DepartmentService(DepartmentRepository repository,
EmployeeRepository employeeRepository) {
this.repository = repository;
this.employeeRepository = employeeRepository;
}
public int employeeCount(Long departmentId) {
if (!repository.existsById(departmentId)) {
throw new DepartmentNotFoundException(departmentId);
}
return employeeRepository.countByDepartmentId(departmentId);
}
}
@ExtendWith(MockitoExtension.class)
class DepartmentServiceTest {
@Mock DepartmentRepository departmentRepository;
@Mock EmployeeRepository employeeRepository;
@InjectMocks DepartmentService departmentService;
@Test
void returnsEmployeeCount() {
when(departmentRepository.existsById(10L)).thenReturn(true);
when(employeeRepository.countByDepartmentId(10L)).thenReturn(7L);
assertThat(departmentService.employeeCount(10L)).isEqualTo(7);
verify(employeeRepository).countByDepartmentId(10L);
}
}
15. Test the controller with @WebMvcTest
@WebMvcTest loads MVC components but not the complete
application. Mock the service and test URL mapping, JSON, validation,
and HTTP status codes.
@WebMvcTest(EmployeeController.class)
class EmployeeControllerTest {
@Autowired MockMvc mvc;
@MockitoBean
EmployeeService employeeService;
@Test
void returnsEmployeeDetails() throws Exception {
when(employeeService.findDetails(1L))
.thenReturn(new EmployeeDetails(1L, "Anand", "Engineering"));
mvc.perform(get("/api/employees/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.employeeName")
.value("Anand"));
}
}
In Spring Boot versions that do not provide
@MockitoBean, use @MockBean. The purpose is
the same: replace the service bean in the MVC test context.
16. Test repositories with @DataJpaTest
@DataJpaTest
class EmployeeRepositoryTest {
@Autowired EmployeeRepository repository;
@Test
void findsEmployeesByDepartment() {
repository.save(new EmployeeEntity("Anand", 10L));
List<EmployeeEntity> employees =
repository.findByDepartmentId(10L);
assertThat(employees).hasSize(1);
assertThat(employees.get(0).getName()).isEqualTo("Anand");
}
}
17. Full integration test
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
class EmployeeApiIntegrationTest {
@Autowired TestRestTemplate client;
@Test
void getsEmployeeFromRunningApplication() {
ResponseEntity<EmployeeDetails> response = client.getForEntity(
"/api/employees/1", EmployeeDetails.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
18. Mockito best practices
- Use mocks for external dependencies, not for the class under test.
- Give each test one clear behavior and use Arrange, Act, Assert.
- Stub only calls needed by the behavior being tested.
- Use real value objects when they are simple; do not mock everything.
- Verify important side effects, but avoid verifying every private detail.
-
Use
@WebMvcTest,@DataJpaTest, and@SpringBootTestat the correct level. - Keep tests independent and repeatable.
Spring Boot Testing FAQs
What is the difference between @WebMvcTest and @SpringBootTest?
@WebMvcTest loads the MVC layer for focused controller tests. @SpringBootTest loads the application context and is better for wiring and integration behavior.
When should Testcontainers be used?
Use Testcontainers when an embedded substitute cannot accurately reproduce the behavior of PostgreSQL, Kafka, Redis, or another external dependency.
Should every service method have a unit test?
Test meaningful behavior and business risk rather than chasing line coverage. A small number of clear tests is more valuable than repetitive tests for implementation details.
Next: ActuatorContinue learning
