Spring Boot 6 min read

Spring Boot Testing with JUnit 5 and Mockito: Complete Guide

Master Spring Boot testing with JUnit 5 and Mockito. Learn unit testing, integration testing, mocking dependencies, and test-driven development.

MR

Moshiour Rahman

Advertisement

Why Testing Matters

Testing ensures your code works correctly, catches bugs early, and enables confident refactoring. Spring Boot provides excellent testing support out of the box.

Testing Pyramid

LevelSpeedScopeTools
UnitFastSingle classJUnit, Mockito
IntegrationMediumMultiple components@SpringBootTest
E2ESlowFull applicationTestRestTemplate

Setup

Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

This includes JUnit 5, Mockito, AssertJ, and Spring Test.

Unit Testing

Testing a Service

// Service to test
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final EmailService emailService;

    public User createUser(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("Email already exists");
        }

        User user = User.builder()
                .name(request.getName())
                .email(request.getEmail())
                .build();

        User savedUser = userRepository.save(user);
        emailService.sendWelcomeEmail(savedUser.getEmail());

        return savedUser;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }
}

Unit Test with Mockito

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_Success() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
        User savedUser = User.builder()
                .id(1L)
                .name("John")
                .email("john@example.com")
                .build();

        when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // When
        User result = userService.createUser(request);

        // Then
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getName()).isEqualTo("John");
        assertThat(result.getEmail()).isEqualTo("john@example.com");

        verify(userRepository).existsByEmail(request.getEmail());
        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail("john@example.com");
    }

    @Test
    void createUser_DuplicateEmail_ThrowsException() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
        when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);

        // When & Then
        assertThatThrownBy(() -> userService.createUser(request))
                .isInstanceOf(DuplicateEmailException.class)
                .hasMessage("Email already exists");

        verify(userRepository, never()).save(any());
        verify(emailService, never()).sendWelcomeEmail(anyString());
    }

    @Test
    void getUserById_NotFound_ThrowsException() {
        // Given
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // When & Then
        assertThatThrownBy(() -> userService.getUserById(userId))
                .isInstanceOf(UserNotFoundException.class)
                .hasMessageContaining("999");
    }
}

Mockito Features

Argument Matchers

// Any value
when(repository.findById(any())).thenReturn(Optional.of(user));
when(repository.findByName(anyString())).thenReturn(users);
when(repository.findByAge(anyInt())).thenReturn(users);

// Specific conditions
when(repository.findById(argThat(id -> id > 0))).thenReturn(Optional.of(user));
when(service.process(argThat(s -> s.startsWith("test")))).thenReturn(result);

// Combining matchers
when(repository.findByNameAndAge(eq("John"), anyInt())).thenReturn(users);

Verifying Interactions

// Verify method called
verify(repository).save(user);

// Verify call count
verify(repository, times(2)).findById(any());
verify(repository, never()).delete(any());
verify(repository, atLeastOnce()).findAll();
verify(repository, atMost(3)).save(any());

// Verify order
InOrder inOrder = inOrder(repository, emailService);
inOrder.verify(repository).save(any());
inOrder.verify(emailService).sendEmail(any());

// Verify no more interactions
verifyNoMoreInteractions(repository);

Argument Captors

@Captor
private ArgumentCaptor<User> userCaptor;

@Test
void createUser_CapturesCorrectUser() {
    // Given
    CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
    when(userRepository.existsByEmail(any())).thenReturn(false);
    when(userRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

    // When
    userService.createUser(request);

    // Then
    verify(userRepository).save(userCaptor.capture());
    User captured = userCaptor.getValue();

    assertThat(captured.getName()).isEqualTo("John");
    assertThat(captured.getEmail()).isEqualTo("john@example.com");
}

Stubbing Consecutive Calls

when(repository.findById(1L))
        .thenReturn(Optional.of(user1))
        .thenReturn(Optional.of(user2))
        .thenThrow(new RuntimeException("Error"));

// Or using thenAnswer for dynamic responses
when(repository.save(any())).thenAnswer(invocation -> {
    User user = invocation.getArgument(0);
    user.setId(++idCounter);
    return user;
});

Testing Controllers

Using @WebMvcTest

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void getUser_Success() throws Exception {
        User user = User.builder()
                .id(1L)
                .name("John")
                .email("john@example.com")
                .build();

        when(userService.getUserById(1L)).thenReturn(user);

        mockMvc.perform(get("/api/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("John"))
                .andExpect(jsonPath("$.email").value("john@example.com"));
    }

    @Test
    void getUser_NotFound() throws Exception {
        when(userService.getUserById(999L))
                .thenThrow(new UserNotFoundException("User not found"));

        mockMvc.perform(get("/api/users/999"))
                .andExpect(status().isNotFound());
    }

    @Test
    void createUser_Success() throws Exception {
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
        User createdUser = User.builder()
                .id(1L)
                .name("John")
                .email("john@example.com")
                .build();

        when(userService.createUser(any())).thenReturn(createdUser);

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("John"));
    }

    @Test
    void createUser_ValidationError() throws Exception {
        CreateUserRequest request = new CreateUserRequest("", "invalid-email");

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest());
    }
}

Integration Testing

Using @SpringBootTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }

    @Test
    void createAndGetUser() {
        // Create user
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");

        ResponseEntity<User> createResponse = restTemplate.postForEntity(
                "/api/users", request, User.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        assertThat(createResponse.getBody().getId()).isNotNull();

        // Get user
        Long userId = createResponse.getBody().getId();
        ResponseEntity<User> getResponse = restTemplate.getForEntity(
                "/api/users/" + userId, User.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getName()).isEqualTo("John");
    }
}

Testing with Real Database

@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmail_ReturnsUser() {
        User user = User.builder()
                .name("John")
                .email("john@example.com")
                .build();
        userRepository.save(user);

        Optional<User> found = userRepository.findByEmail("john@example.com");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
}

Testing Data Layer

@DataJpaTest

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmail_Success() {
        // Given
        User user = User.builder()
                .name("John")
                .email("john@example.com")
                .build();
        entityManager.persistAndFlush(user);

        // When
        Optional<User> found = userRepository.findByEmail("john@example.com");

        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }

    @Test
    void findByEmail_NotFound() {
        Optional<User> found = userRepository.findByEmail("notfound@example.com");
        assertThat(found).isEmpty();
    }

    @Test
    void findActiveUsers_ReturnsOnlyActiveUsers() {
        User activeUser = User.builder().name("Active").email("active@test.com").active(true).build();
        User inactiveUser = User.builder().name("Inactive").email("inactive@test.com").active(false).build();

        entityManager.persist(activeUser);
        entityManager.persist(inactiveUser);
        entityManager.flush();

        List<User> activeUsers = userRepository.findByActiveTrue();

        assertThat(activeUsers).hasSize(1);
        assertThat(activeUsers.get(0).getName()).isEqualTo("Active");
    }
}

Testing Configuration

Using @TestConfiguration

@TestConfiguration
public class TestConfig {

    @Bean
    public EmailService emailService() {
        return new FakeEmailService();
    }
}

@SpringBootTest
@Import(TestConfig.class)
class UserServiceIntegrationTest {
    // EmailService will use FakeEmailService
}

Test Properties

@SpringBootTest
@TestPropertySource(properties = {
    "app.feature.enabled=true",
    "app.cache.ttl=60"
})
class FeatureTest {
    // Tests with custom properties
}

// Or using a properties file
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class ConfigTest {
}

Testing Async Code

@Service
public class AsyncService {

    @Async
    public CompletableFuture<String> processAsync(String input) {
        return CompletableFuture.completedFuture("Processed: " + input);
    }
}

@SpringBootTest
class AsyncServiceTest {

    @Autowired
    private AsyncService asyncService;

    @Test
    void processAsync_Success() throws Exception {
        CompletableFuture<String> future = asyncService.processAsync("test");

        String result = future.get(5, TimeUnit.SECONDS);

        assertThat(result).isEqualTo("Processed: test");
    }
}

Testing Security

@WebMvcTest(SecureController.class)
class SecureControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void accessSecureEndpoint_Unauthorized() throws Exception {
        mockMvc.perform(get("/api/secure"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    void accessSecureEndpoint_Authorized() throws Exception {
        mockMvc.perform(get("/api/secure"))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void accessAdminEndpoint_AsAdmin() throws Exception {
        mockMvc.perform(get("/api/admin"))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    void accessAdminEndpoint_AsUser_Forbidden() throws Exception {
        mockMvc.perform(get("/api/admin"))
                .andExpect(status().isForbidden());
    }
}

Best Practices

Test Naming Convention

// Method: methodName_condition_expectedResult
@Test
void getUserById_ValidId_ReturnsUser() { }

@Test
void getUserById_InvalidId_ThrowsException() { }

@Test
void createUser_DuplicateEmail_ThrowsDuplicateException() { }

Arrange-Act-Assert Pattern

@Test
void calculateDiscount_PremiumUser_Returns20Percent() {
    // Arrange
    User user = User.builder().type(UserType.PREMIUM).build();
    Order order = Order.builder().total(100.0).build();

    // Act
    double discount = discountService.calculate(user, order);

    // Assert
    assertThat(discount).isEqualTo(20.0);
}

Test Data Builders

public class UserTestBuilder {
    private Long id = 1L;
    private String name = "Default Name";
    private String email = "default@example.com";

    public static UserTestBuilder aUser() {
        return new UserTestBuilder();
    }

    public UserTestBuilder withId(Long id) {
        this.id = id;
        return this;
    }

    public UserTestBuilder withName(String name) {
        this.name = name;
        return this;
    }

    public UserTestBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    public User build() {
        return User.builder()
                .id(id)
                .name(name)
                .email(email)
                .build();
    }
}

// Usage
User user = aUser().withName("John").withEmail("john@test.com").build();

Summary

Test TypeAnnotationUse Case
Unit@ExtendWith(MockitoExtension.class)Single class
Controller@WebMvcTestREST endpoints
Repository@DataJpaTestJPA queries
Integration@SpringBootTestFull context

Effective testing makes your Spring Boot applications reliable and maintainable.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.