Spring Boot Testing

In simple terms: A test checks whether a small piece of code behaves as expected. Mockito creates fake dependencies so you can test an Employee or Department service without connecting to a real database, sending an email, or calling another service.
A practical testing pyramid
Unit testService + Mockito mocks
Slice testController or repository
Integration testReal application path

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 @SpringBootTest at the correct level.
  • Keep tests independent and repeatable.
Remember: Mockito is for replacing dependencies in a focused test. It does not prove that your database, Spring wiring, HTTP configuration, and real application work together; use slice and integration tests for that confidence.

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: Actuator

Continue learning