Menu

Spring Boot Topic

Spring Boot Testing

1. Introduction to Spring Boot Testing

Spring Boot Testing provides a comprehensive set of tools and annotations for testing Spring Boot applications. It builds on the Spring TestContext Framework and provides auto-configuration for various testing scenarios.

Key Features:

  • Auto-configuration: Automatic setup of application context

  • Test Slices: Test specific layers of your application

  • Mocking Support: Built-in support for mocking beans

  • Test Containers: Integration with Testcontainers for database testing

  • Web Testing: Support for testing MVC controllers and WebFlux

Testing Pyramid:

  • Unit Tests (70%): Test individual components in isolation

  • Integration Tests (20%): Test interactions between components

  • End-to-End Tests (10%): Test complete application flow

2. Test Dependencies

Maven Dependencies

<dependencies>
    <!-- Spring Boot Starter Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Test Containers -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Additional Testing Libraries -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

What's Included in spring-boot-starter-test:

  • JUnit 5: The default testing framework

  • Spring Test & Spring Boot Test: Utilities and integration test support

  • AssertJ: Fluent assertions library

  • Hamcrest: Matcher library

  • Mockito: Mocking framework

  • JSONassert: JSON assertion library

  • JsonPath: XPath for JSON

3. Unit Testing

3.1 Testing Service Layer with Mocking

Service Class:

@Service
@Transactional
public class UserService {
    
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    public User createUser(User user) {
        if (userRepository.existsByEmail(user.getEmail())) {
            throw new UserAlreadyExistsException("Email already exists: " + user.getEmail());
        }
        
        User savedUser = userRepository.save(user);
        emailService.sendWelcomeEmail(user.getEmail());
        
        return savedUser;
    }
    
    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }
    
    public List<User> searchUsers(String keyword) {
        return userRepository.findByUsernameContainingOrEmailContaining(keyword, keyword);
    }
}

Unit Test:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void whenCreateUserWithValidData_thenUserIsCreated() {
        // Given
        User user = new User("john.doe", "john@example.com");
        User savedUser = new User(1L, "john.doe", "john@example.com");
        
        when(userRepository.existsByEmail("john@example.com")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        doNothing().when(emailService).sendWelcomeEmail(anyString());
        
        // When
        User result = userService.createUser(user);
        
        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getUsername()).isEqualTo("john.doe");
        
        verify(userRepository).existsByEmail("john@example.com");
        verify(userRepository).save(user);
        verify(emailService).sendWelcomeEmail("john@example.com");
    }
    
    @Test
    void whenCreateUserWithExistingEmail_thenThrowException() {
        // Given
        User user = new User("john.doe", "existing@example.com");
        
        when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
        
        // When & Then
        assertThatThrownBy(() -> userService.createUser(user))
            .isInstanceOf(UserAlreadyExistsException.class)
            .hasMessage("Email already exists: existing@example.com");
        
        verify(userRepository, never()).save(any(User.class));
        verify(emailService, never()).sendWelcomeEmail(anyString());
    }
    
    @Test
    void whenGetUserById_thenReturnUser() {
        // Given
        User user = new User(1L, "john.doe", "john@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        // When
        Optional<User> result = userService.getUserById(1L);
        
        // Then
        assertThat(result).isPresent();
        assertThat(result.get().getUsername()).isEqualTo("john.doe");
    }
    
    @Test
    void whenGetNonExistentUser_thenReturnEmpty() {
        // Given
        when(userRepository.findById(999L)).thenReturn(Optional.empty());
        
        // When
        Optional<User> result = userService.getUserById(999L);
        
        // Then
        assertThat(result).isEmpty();
    }
}

3.2 Testing Utility Classes

Utility Class:

@Component
public class PasswordValidator {
    
    private static final String PASSWORD_PATTERN = 
        "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
    
    public boolean isValid(String password) {
        if (password == null) {
            return false;
        }
        return password.matches(PASSWORD_PATTERN);
    }
    
    public List<String> validatePassword(String password) {
        List<String> errors = new ArrayList<>();
        
        if (password == null || password.length() < 8) {
            errors.add("Password must be at least 8 characters long");
        }
        if (password != null && !password.matches(".*[0-9].*")) {
            errors.add("Password must contain at least one digit");
        }
        if (password != null && !password.matches(".*[a-z].*")) {
            errors.add("Password must contain at least one lowercase letter");
        }
        if (password != null && !password.matches(".*[A-Z].*")) {
            errors.add("Password must contain at least one uppercase letter");
        }
        
        return errors;
    }
}

Unit Test:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;

class PasswordValidatorTest {
    
    private PasswordValidator passwordValidator;
    
    @BeforeEach
    void setUp() {
        passwordValidator = new PasswordValidator();
    }
    
    @ParameterizedTest
    @ValueSource(strings = {
        "ValidPass1!",
        "StrongP@ss123",
        "Test123#Password"
    })
    @DisplayName("Should return true for valid passwords")
    void whenPasswordIsValid_thenReturnTrue(String password) {
        // When & Then
        assertThat(passwordValidator.isValid(password)).isTrue();
    }
    
    @ParameterizedTest
    @ValueSource(strings = {
        "weak",
        "nouppercase1!",
        "NOLOWERCASE1!",
        "NoSpecialChar123",
        "Short1!"
    })
    @DisplayName("Should return false for invalid passwords")
    void whenPasswordIsInvalid_thenReturnFalse(String password) {
        // When & Then
        assertThat(passwordValidator.isValid(password)).isFalse();
    }
    
    @Test
    @DisplayName("Should return specific validation errors")
    void whenPasswordValidationFails_thenReturnSpecificErrors() {
        // Given
        String weakPassword = "weak";
        
        // When
        List<String> errors = passwordValidator.validatePassword(weakPassword);
        
        // Then
        assertThat(errors)
            .hasSize(4)
            .contains(
                "Password must be at least 8 characters long",
                "Password must contain at least one digit",
                "Password must contain at least one uppercase letter",
                "Password must contain at least one lowercase letter"
            );
    }
    
    @Test
    @DisplayName("Should return empty list for valid password")
    void whenPasswordIsValid_thenReturnEmptyErrorList() {
        // Given
        String validPassword = "ValidPass1!";
        
        // When
        List<String> errors = passwordValidator.validatePassword(validPassword);
        
        // Then
        assertThat(errors).isEmpty();
    }
}

4. Integration Testing

4.1 Data JPA Tests

Repository Test:

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = {
    "spring.jpa.hibernate.ddl-auto=create-drop",
    "spring.jpa.show-sql=true"
})
class UserRepositoryIntegrationTest {
    
    @Autowired
    private TestEntityManager testEntityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void whenFindByEmail_thenReturnUser() {
        // Given
        User user = new User("jane.doe", "jane@example.com");
        testEntityManager.persistAndFlush(user);
        
        // When
        Optional<User> found = userRepository.findByEmail("jane@example.com");
        
        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getUsername()).isEqualTo("jane.doe");
    }
    
    @Test
    void whenFindByNonExistentEmail_thenReturnEmpty() {
        // When
        Optional<User> found = userRepository.findByEmail("nonexistent@example.com");
        
        // Then
        assertThat(found).isEmpty();
    }
    
    @Test
    void whenExistsByEmail_thenReturnTrue() {
        // Given
        User user = new User("john.doe", "john@example.com");
        testEntityManager.persistAndFlush(user);
        
        // When
        boolean exists = userRepository.existsByEmail("john@example.com");
        
        // Then
        assertThat(exists).isTrue();
    }
    
    @Test
    void whenFindByUsernameContaining_thenReturnUsers() {
        // Given
        User user1 = new User("john.doe", "john@example.com");
        User user2 = new User("jane.doe", "jane@example.com");
        User user3 = new User("bob.smith", "bob@example.com");
        
        testEntityManager.persist(user1);
        testEntityManager.persist(user2);
        testEntityManager.persist(user3);
        testEntityManager.flush();
        
        // When
        List<User> users = userRepository.findByUsernameContaining("doe");
        
        // Then
        assertThat(users)
            .hasSize(2)
            .extracting(User::getUsername)
            .containsExactlyInAnyOrder("john.doe", "jane.doe");
    }
}

4.2 Service Layer Integration Test

Service Integration Test:

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
class UserServiceIntegrationTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void whenCreateUser_thenUserIsPersisted() {
        // Given
        User user = new User("test.user", "test@example.com");
        
        // When
        User savedUser = userService.createUser(user);
        
        // Then
        assertThat(savedUser.getId()).isNotNull();
        
        Optional<User> foundUser = userRepository.findById(savedUser.getId());
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getUsername()).isEqualTo("test.user");
    }
    
    @Test
    void whenCreateUserWithDuplicateEmail_thenThrowException() {
        // Given
        User user1 = new User("user1", "duplicate@example.com");
        userService.createUser(user1);
        
        User user2 = new User("user2", "duplicate@example.com");
        
        // When & Then
        assertThatThrownBy(() -> userService.createUser(user2))
            .isInstanceOf(UserAlreadyExistsException.class);
    }
}

5. Web Layer Testing

5.1 Testing REST Controllers with @WebMvcTest

Controller:

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody @Valid CreateUserRequest request) {
        User user = new User(request.getUsername(), request.getEmail());
        User createdUser = userService.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, 
                                         @RequestBody @Valid UpdateUserRequest request) {
        User user = userService.getUserById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
        
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        
        User updatedUser = userService.updateUser(user);
        return ResponseEntity.ok(updatedUser);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

Controller Test:

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @MockBean
    private UserService userService;
    
    @Test
    void whenGetAllUsers_thenReturnUsers() throws Exception {
        // Given
        List<User> users = Arrays.asList(
            new User(1L, "john.doe", "john@example.com"),
            new User(2L, "jane.doe", "jane@example.com")
        );
        
        when(userService.getAllUsers()).thenReturn(users);
        
        // When & Then
        mockMvc.perform(get("/api/users")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].username").value("john.doe"))
                .andExpect(jsonPath("$[1].username").value("jane.doe"));
    }
    
    @Test
    void whenGetExistingUser_thenReturnUser() throws Exception {
        // Given
        User user = new User(1L, "john.doe", "john@example.com");
        when(userService.getUserById(1L)).thenReturn(Optional.of(user));
        
        // When & Then
        mockMvc.perform(get("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.username").value("john.doe"))
                .andExpect(jsonPath("$.email").value("john@example.com"));
    }
    
    @Test
    void whenGetNonExistentUser_thenReturnNotFound() throws Exception {
        // Given
        when(userService.getUserById(999L)).thenReturn(Optional.empty());
        
        // When & Then
        mockMvc.perform(get("/api/users/999")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound());
    }
    
    @Test
    void whenCreateValidUser_thenReturnCreated() throws Exception {
        // Given
        CreateUserRequest request = new CreateUserRequest("new.user", "new@example.com");
        User createdUser = new User(1L, "new.user", "new@example.com");
        
        when(userService.createUser(any(User.class))).thenReturn(createdUser);
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.username").value("new.user"))
                .andExpect(jsonPath("$.email").value("new@example.com"));
    }
    
    @Test
    void whenCreateUserWithInvalidData_thenReturnBadRequest() throws Exception {
        // Given
        CreateUserRequest request = new CreateUserRequest("", "invalid-email");
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errors").exists());
    }
    
    @Test
    void whenUpdateUser_thenReturnUpdatedUser() throws Exception {
        // Given
        UpdateUserRequest request = new UpdateUserRequest("updated.user", "updated@example.com");
        User existingUser = new User(1L, "old.user", "old@example.com");
        User updatedUser = new User(1L, "updated.user", "updated@example.com");
        
        when(userService.getUserById(1L)).thenReturn(Optional.of(existingUser));
        when(userService.updateUser(any(User.class))).thenReturn(updatedUser);
        
        // When & Then
        mockMvc.perform(put("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.username").value("updated.user"))
                .andExpect(jsonPath("$.email").value("updated@example.com"));
    }
    
    @Test
    void whenDeleteUser_thenReturnNoContent() throws Exception {
        // Given
        doNothing().when(userService).deleteUser(1L);
        
        // When & Then
        mockMvc.perform(delete("/api/users/1"))
                .andExpect(status().isNoContent());
        
        verify(userService).deleteUser(1L);
    }
}

5.2 Testing with @SpringBootTest and TestRestTemplate

Integration Test with Real HTTP Calls:

import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserControllerIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    private String baseUrl;
    
    @BeforeEach
    void setUp() {
        baseUrl = "http://localhost:" + port + "/api/users";
        userRepository.deleteAll();
    }
    
    @Test
    @Order(1)
    void whenCreateUser_thenUserIsCreated() {
        // Given
        CreateUserRequest request = new CreateUserRequest("integration.user", "integration@example.com");
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<CreateUserRequest> entity = new HttpEntity<>(request, headers);
        
        // When
        ResponseEntity<User> response = restTemplate.exchange(
            baseUrl, HttpMethod.POST, entity, User.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getId()).isNotNull();
        assertThat(response.getBody().getUsername()).isEqualTo("integration.user");
        
        // Verify in database
        Optional<User> savedUser = userRepository.findByEmail("integration@example.com");
        assertThat(savedUser).isPresent();
    }
    
    @Test
    @Order(2)
    void whenGetUser_thenReturnUser() {
        // Given - Create user first
        User user = new User("test.user", "test@example.com");
        User savedUser = userRepository.save(user);
        
        // When
        ResponseEntity<User> response = restTemplate.getForEntity(
            baseUrl + "/" + savedUser.getId(), User.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getUsername()).isEqualTo("test.user");
    }
}

6. Test Slices

Available Test Slices:

Annotation Purpose Auto-configures
@WebMvcTest MVC controller tests MVC infrastructure, @Controller beans
@DataJpaTest JPA repository tests DataSource, EntityManager, @Repository beans
@JsonTest JSON serialization tests JSON mappers (Jackson, Gson)
@DataMongoTest MongoDB tests MongoTemplate, @Document classes
@RestClientTest REST client tests REST client infrastructure
@WebFluxTest WebFlux controller tests WebFlux infrastructure

Example of Different Test Slices:

JSON Test:

@JsonTest
class UserJsonTest {
    
    @Autowired
    private JacksonTester<User> json;
    
    @Test
    void whenSerializeUser_thenCorrectJson() throws Exception {
        User user = new User(1L, "john.doe", "john@example.com");
        
        assertThat(json.write(user))
            .hasJsonPathNumberValue("$.id")
            .hasJsonPathStringValue("$.username")
            .hasJsonPathStringValue("$.email")
            .extractingJsonPathStringValue("$.username").isEqualTo("john.doe");
    }
    
    @Test
    void whenDeserializeJson_thenCorrectUser() throws Exception {
        String userJson = """
            {
                "id": 1,
                "username": "john.doe",
                "email": "john@example.com"
            }
            """;
        
        assertThat(json.parse(userJson))
            .isEqualTo(new User(1L, "john.doe", "john@example.com"));
    }
}

7. Test Configuration and Profiles

Test Configuration Class:

@TestConfiguration
public class TestConfig {
    
    @Bean
    @Primary
    public UserService testUserService() {
        return new UserService(testUserRepository(), testEmailService());
    }
    
    @Bean
    @Primary
    public UserRepository testUserRepository() {
        return new InMemoryUserRepository();
    }
    
    @Bean
    @Primary 
    public EmailService testEmailService() {
        return mock(EmailService.class);
    }
}

// In-memory repository for testing
public class InMemoryUserRepository implements UserRepository {
    private final Map<Long, User> users = new HashMap<>();
    private Long currentId = 1L;
    
    @Override
    public User save(User user) {
        if (user.getId() == null) {
            user.setId(currentId++);
        }
        users.put(user.getId(), user);
        return user;
    }
    
    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(users.get(id));
    }
    
    // Implement other methods...
}

Profile-specific Test Configuration:

@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "spring.jpa.hibernate.ddl-auto=create-drop",
    "logging.level.org.springframework.security=DEBUG"
})
class ProfileSpecificTest {
    
    @Test
    void whenTestProfileActive_thenUseTestConfiguration() {
        // Test implementation
    }
}

8. Advanced Testing Scenarios

8.1 Testing with Testcontainers

Database Integration Test:

import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class UserRepositoryTestcontainersTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @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 whenSaveUserInContainer_thenUserIsPersisted() {
        // Given
        User user = new User("container.user", "container@example.com");
        
        // When
        User savedUser = userRepository.save(user);
        
        // Then
        assertThat(savedUser.getId()).isNotNull();
        assertThat(userRepository.count()).isEqualTo(1);
    }
}

8.2 Testing Security

Security Test:

@WebMvcTest(UserController.class)
@WithMockUser
class UserControllerSecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    @WithMockUser(roles = "USER")
    void whenUserWithUserRoleAccessEndpoint_thenSuccess() throws Exception {
        when(userService.getAllUsers()).thenReturn(Collections.emptyList());
        
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void whenUserWithAdminRoleAccessEndpoint_thenSuccess() throws Exception {
        // Test admin access
    }
    
    @Test
    @WithAnonymousUser
    void whenAnonymousUserAccessSecuredEndpoint_thenUnauthorized() throws Exception {
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isUnauthorized());
    }
}

9. Best Practices

1. Test Structure (Given-When-Then)

@Test
void whenCondition_thenExpectedResult() {
    // Given - setup test data and preconditions
    User user = new User("test.user", "test@example.com");
    
    // When - execute the action being tested
    User result = userService.createUser(user);
    
    // Then - verify the results
    assertThat(result).isNotNull();
    assertThat(result.getId()).isNotNull();
}

2. Meaningful Test Names

@Test
void whenCreateUserWithDuplicateEmail_thenThrowUserAlreadyExistsException() { }

@Test
void whenSearchUsersWithEmptyKeyword_thenReturnAllUsers() { }

@Test
void whenGetNonExistentUserById_thenReturnEmptyOptional() { }

3. Use AssertJ for Readable Assertions

assertThat(actualList)
    .hasSize(3)
    .containsExactlyInAnyOrder("item1", "item2", "item3")
    .allMatch(item -> item.startsWith("item"));

4. Test Isolation

@BeforeEach
void setUp() {
    // Reset state before each test
    userRepository.deleteAll();
}

@AfterEach
void tearDown() {
    // Clean up after each test
    testEntityManager.clear();
}

5. Use Test Profiles

# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
logging.level.com.example=DEBUG

This comprehensive guide covers Spring Boot Testing from unit tests to integration tests, including advanced scenarios with Testcontainers and security testing. The key is to choose the right testing strategy based on what you're testing and use the appropriate test slices to keep tests focused and fast.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?