1. java
  2. /spring
  3. /spring-testing

Complete Guide to Spring Testing Framework

Spring Testing

Spring provides comprehensive testing support that makes it easy to write unit tests, integration tests, and end-to-end tests. The Spring Test framework integrates seamlessly with JUnit 5, Mockito, and other testing tools to provide a robust testing foundation for Spring applications.

Testing Fundamentals

Spring testing is built around several key concepts:

Test Context Framework: Manages Spring ApplicationContext for tests Test Slices: Focused testing of specific application layers Mock Integration: Built-in support for mocking Spring beans Test Annotations: Specialized annotations for different testing scenarios

// Basic Spring Test Setup
@SpringBootTest
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb",
    "spring.jpa.hibernate.ddl-auto=create-drop"
})
class ApplicationIntegrationTest {
    
    @Autowired
    private UserService userService;
    
    @MockBean
    private EmailService emailService;
    
    @Test
    void contextLoads() {
        assertThat(userService).isNotNull();
    }
    
    @Test
    void shouldCreateUser() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]");
        
        // When
        User created = userService.createUser(request);
        
        // Then
        assertThat(created.getId()).isNotNull();
        verify(emailService).sendWelcomeEmail(created);
    }
}

Test Annotations

Core Test Annotations

// @SpringBootTest - Full integration test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FullIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void shouldHandleFullRequest() {
        ResponseEntity<String> response = restTemplate.getForEntity("/api/health", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

// @WebMvcTest - Web layer testing
@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldReturnUser() throws Exception {
        // Given
        User user = new User(1L, "John", "[email protected]");
        when(userService.findById(1L)).thenReturn(user);
        
        // When & Then
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"))
            .andExpected(jsonPath("$.email").value("[email protected]"));
    }
}

// @DataJpaTest - JPA repository testing
@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void shouldFindByEmail() {
        // Given
        User user = new User("John", "[email protected]");
        entityManager.persistAndFlush(user);
        
        // When
        Optional<User> found = userRepository.findByEmail("[email protected]");
        
        // Then
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }
}

// @JsonTest - JSON serialization testing
@JsonTest
class UserJsonTest {
    
    @Autowired
    private JacksonTester<User> json;
    
    @Test
    void shouldSerializeUser() throws Exception {
        User user = new User(1L, "John", "[email protected]");
        
        assertThat(json.write(user)).extractingJsonPathStringValue("$.name")
            .isEqualTo("John");
        assertThat(json.write(user)).extractingJsonPathStringValue("$.email")
            .isEqualTo("[email protected]");
    }
    
    @Test
    void shouldDeserializeUser() throws Exception {
        String content = """
            {
                "id": 1,
                "name": "John",
                "email": "[email protected]"
            }
            """;
        
        User user = json.parseObject(content);
        assertThat(user.getName()).isEqualTo("John");
        assertThat(user.getEmail()).isEqualTo("[email protected]");
    }
}

Test Configuration

// Test Configuration Class
@TestConfiguration
public class TestConfig {
    
    @Bean
    @Primary
    public Clock testClock() {
        return Clock.fixed(Instant.parse("2023-01-01T00:00:00Z"), ZoneOffset.UTC);
    }
    
    @Bean
    @Primary
    public EmailService mockEmailService() {
        return Mockito.mock(EmailService.class);
    }
}

// Profile-specific test configuration
@TestConfiguration
@Profile("test")
public class TestDataConfig {
    
    @Bean
    @Primary
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }
}

Unit Testing

Service Layer Testing

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @Mock
    private PasswordEncoder passwordEncoder;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void shouldCreateUser() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]", "password");
        User savedUser = new User(1L, "John", "[email protected]");
        
        when(passwordEncoder.encode("password")).thenReturn("encoded");
        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");
        
        verify(userRepository).save(argThat(user -> 
            user.getName().equals("John") && 
            user.getPassword().equals("encoded")));
        verify(emailService).sendWelcomeEmail(savedUser);
    }
    
    @Test
    void shouldThrowExceptionWhenUserNotFound() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.empty());
        
        // When & Then
        assertThrows(UserNotFoundException.class, () -> 
            userService.getUserById(1L));
    }
    
    @Test
    void shouldUpdateUserSuccessfully() {
        // Given
        User existingUser = new User(1L, "John", "[email protected]");
        UpdateUserRequest request = new UpdateUserRequest("John Doe", "[email protected]");
        
        when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class))).thenReturn(existingUser);
        
        // When
        User result = userService.updateUser(1L, request);
        
        // Then
        assertThat(result.getName()).isEqualTo("John Doe");
        assertThat(result.getEmail()).isEqualTo("[email protected]");
        
        verify(userRepository).save(existingUser);
    }
}

Custom Argument Matchers

public class UserMatchers {
    
    public static User userWithName(String name) {
        return argThat(user -> user != null && name.equals(user.getName()));
    }
    
    public static User userWithEmail(String email) {
        return argThat(user -> user != null && email.equals(user.getEmail()));
    }
    
    public static CreateUserRequest validCreateRequest() {
        return argThat(request -> 
            request != null && 
            request.getName() != null && 
            request.getEmail() != null &&
            request.getEmail().contains("@"));
    }
}

// Usage in tests
@Test
void shouldCreateUserWithValidRequest() {
    // Given
    when(userRepository.save(userWithName("John"))).thenReturn(savedUser);
    
    // When
    userService.createUser(request);
    
    // Then
    verify(userRepository).save(userWithName("John"));
}

Integration Testing

Web Layer Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @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);
    }
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    void shouldCreateAndRetrieveUser() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]", "password");
        
        // When - Create user
        ResponseEntity<User> createResponse = restTemplate.postForEntity("/api/users", request, User.class);
        
        // Then - Verify creation
        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        User createdUser = createResponse.getBody();
        assertThat(createdUser.getId()).isNotNull();
        
        // When - Retrieve user
        ResponseEntity<User> getResponse = restTemplate.getForEntity(
            "/api/users/" + createdUser.getId(), User.class);
        
        // Then - Verify retrieval
        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getName()).isEqualTo("John");
    }
    
    @Test
    void shouldReturnValidationErrorForInvalidRequest() {
        // Given
        CreateUserRequest request = new CreateUserRequest("", "invalid-email", "");
        
        // When
        ResponseEntity<ErrorResponse> response = restTemplate.postForEntity(
            "/api/users", request, ErrorResponse.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(response.getBody().getCode()).isEqualTo("VALIDATION_FAILED");
    }
}

Transaction Testing

@SpringBootTest
@Transactional
class TransactionalServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Rollback
    void shouldRollbackOnException() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]", "password");
        
        // When & Then
        assertThrows(EmailSendingException.class, () -> 
            userService.createUserWithEmailNotification(request));
        
        // Verify rollback occurred
        assertThat(userRepository.findAll()).isEmpty();
    }
    
    @Test
    @Commit
    void shouldCommitSuccessfulOperation() {
        // Given
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]", "password");
        
        // When
        User created = userService.createUser(request);
        
        // Then
        assertThat(created.getId()).isNotNull();
        assertThat(userRepository.count()).isEqualTo(1);
    }
}

Test Slices

Repository Testing

@DataJpaTest
class ProductRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    void shouldFindByCategory() {
        // Given
        Product electronics = new Product("Laptop", "Electronics", 999.99);
        Product clothing = new Product("Shirt", "Clothing", 29.99);
        
        entityManager.persistAndFlush(electronics);
        entityManager.persistAndFlush(clothing);
        
        // When
        List<Product> electronicsProducts = productRepository.findByCategory("Electronics");
        
        // Then
        assertThat(electronicsProducts).hasSize(1);
        assertThat(electronicsProducts.get(0).getName()).isEqualTo("Laptop");
    }
    
    @Test
    void shouldSupportPagination() {
        // Given
        for (int i = 0; i < 15; i++) {
            Product product = new Product("Product" + i, "Category", 10.0 + i);
            entityManager.persistAndFlush(product);
        }
        
        // When
        Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
        Page<Product> page = productRepository.findAll(pageable);
        
        // Then
        assertThat(page.getContent()).hasSize(5);
        assertThat(page.getTotalElements()).isEqualTo(15);
        assertThat(page.getTotalPages()).isEqualTo(3);
    }
    
    @Test
    void shouldExecuteCustomQuery() {
        // Given
        Product expensive = new Product("Expensive", "Luxury", 1000.0);
        Product cheap = new Product("Cheap", "Budget", 10.0);
        
        entityManager.persistAndFlush(expensive);
        entityManager.persistAndFlush(cheap);
        
        // When
        List<Product> expensiveProducts = productRepository.findProductsAbovePrice(500.0);
        
        // Then
        assertThat(expensiveProducts).hasSize(1);
        assertThat(expensiveProducts.get(0).getName()).isEqualTo("Expensive");
    }
}

Web MVC Testing

@WebMvcTest(ProductController.class)
class ProductControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private ProductService productService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void shouldCreateProduct() throws Exception {
        // Given
        CreateProductRequest request = new CreateProductRequest("Laptop", "Electronics", 999.99);
        Product savedProduct = new Product(1L, "Laptop", "Electronics", 999.99);
        
        when(productService.createProduct(any(CreateProductRequest.class))).thenReturn(savedProduct);
        
        // When & Then
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpected(status().isCreated())
            .andExpect(jsonPath("$.id").value(1))
            .andExpected(jsonPath("$.name").value("Laptop"))
            .andExpected(jsonPath("$.category").value("Electronics"));
    }
    
    @Test
    void shouldReturnBadRequestForInvalidProduct() throws Exception {
        // Given
        CreateProductRequest request = new CreateProductRequest("", "Electronics", -1.0);
        
        // When & Then
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpected(status().isBadRequest())
            .andExpected(jsonPath("$.code").value("VALIDATION_FAILED"));
    }
    
    @Test
    void shouldHandleProductNotFound() throws Exception {
        // Given
        when(productService.getProduct(999L)).thenThrow(new ProductNotFoundException("Product not found"));
        
        // When & Then
        mockMvc.perform(get("/api/products/999"))
            .andExpected(status().isNotFound())
            .andExpected(jsonPath("$.code").value("PRODUCT_NOT_FOUND"));
    }
}

Security Testing

@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void shouldAllowPublicAccess() throws Exception {
        mockMvc.perform(get("/api/public/health"))
            .andExpected(status().isOk());
    }
    
    @Test
    void shouldRequireAuthenticationForProtectedEndpoints() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpected(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    void shouldAllowUserAccess() throws Exception {
        mockMvc.perform(get("/api/users/me"))
            .andExpected(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void shouldAllowAdminAccess() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpected(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    void shouldDenyUserAccessToAdminEndpoints() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpected(status().isForbidden());
    }
}

// Custom Security Test Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "admin", roles = "ADMIN")
public @interface WithMockAdmin {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "user", roles = "USER")
public @interface WithMockRegularUser {
}

// Usage
@Test
@WithMockAdmin
void shouldAllowAdminToDeleteUser() throws Exception {
    mockMvc.perform(delete("/api/users/1"))
        .andExpected(status().isNoContent());
}

Test Data Management

Test Data Builders

// Test Data Builder Pattern
public class UserTestDataBuilder {
    
    private String name = "Default Name";
    private String email = "[email protected]";
    private Integer age = 25;
    private boolean active = true;
    private UserRole role = UserRole.USER;
    
    public static UserTestDataBuilder aUser() {
        return new UserTestDataBuilder();
    }
    
    public UserTestDataBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public UserTestDataBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public UserTestDataBuilder withAge(Integer age) {
        this.age = age;
        return this;
    }
    
    public UserTestDataBuilder inactive() {
        this.active = false;
        return this;
    }
    
    public UserTestDataBuilder asAdmin() {
        this.role = UserRole.ADMIN;
        return this;
    }
    
    public User build() {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        user.setAge(age);
        user.setActive(active);
        user.setRole(role);
        return user;
    }
    
    public CreateUserRequest buildRequest() {
        return new CreateUserRequest(name, email, "password123");
    }
}

// Usage in tests
@Test
void shouldCreateActiveUser() {
    // Given
    User user = aUser()
        .withName("John Doe")
        .withEmail("[email protected]")
        .withAge(30)
        .build();
    
    // Test implementation
}

Database Test Setup

@TestConfiguration
public class TestDatabaseConfig {
    
    @Bean
    @Primary
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:test-schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
    }
}

// SQL Test Data
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class UserRepositoryIntegrationTest {
    
    @Test
    void shouldFindTestUsers() {
        List<User> users = userRepository.findAll();
        assertThat(users).isNotEmpty();
    }
}

Performance Testing

@SpringBootTest
class PerformanceTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    @Timeout(value = 2, unit = TimeUnit.SECONDS)
    void shouldCreateUserWithinTimeLimit() {
        CreateUserRequest request = new CreateUserRequest("John", "[email protected]", "password");
        
        User created = userService.createUser(request);
        
        assertThat(created.getId()).isNotNull();
    }
    
    @Test
    void shouldHandleConcurrentUserCreation() throws InterruptedException {
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);
        List<User> createdUsers = Collections.synchronizedList(new ArrayList<>());
        
        for (int i = 0; i < threadCount; i++) {
            final int userId = i;
            new Thread(() -> {
                try {
                    CreateUserRequest request = new CreateUserRequest(
                        "User" + userId, 
                        "user" + userId + "@example.com", 
                        "password"
                    );
                    User created = userService.createUser(request);
                    createdUsers.add(created);
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        latch.await(10, TimeUnit.SECONDS);
        
        assertThat(createdUsers).hasSize(threadCount);
    }
}

Test Best Practices

// Test Naming and Organization
class UserServiceTest {
    
    @Nested
    @DisplayName("User Creation")
    class UserCreation {
        
        @Test
        @DisplayName("Should create user with valid data")
        void shouldCreateUser_WhenValidData_ThenReturnCreatedUser() {
            // Test implementation
        }
        
        @Test
        @DisplayName("Should throw exception when email already exists")
        void shouldThrowException_WhenEmailExists_ThenThrowDuplicateEmailException() {
            // Test implementation
        }
    }
    
    @Nested
    @DisplayName("User Retrieval")
    class UserRetrieval {
        
        @Test
        @DisplayName("Should return user when found")
        void shouldReturnUser_WhenUserExists_ThenReturnUser() {
            // Test implementation
        }
        
        @Test
        @DisplayName("Should throw exception when user not found")
        void shouldThrowException_WhenUserNotFound_ThenThrowNotFoundException() {
            // Test implementation
        }
    }
}

// Parameterized Tests
@ParameterizedTest
@ValueSource(strings = {"", " ", "a", "ab"})
@DisplayName("Should reject invalid usernames")
void shouldRejectInvalidUsernames(String invalidUsername) {
    CreateUserRequest request = new CreateUserRequest(invalidUsername, "[email protected]", "password");
    
    assertThrows(ValidationException.class, () -> userService.createUser(request));
}

@ParameterizedTest
@CsvSource({
    "[email protected], true",
    "invalid-email, false",
    "@example.com, false",
    "john@, false"
})
@DisplayName("Should validate email formats")
void shouldValidateEmailFormats(String email, boolean isValid) {
    boolean result = EmailValidator.isValid(email);
    assertThat(result).isEqualTo(isValid);
}

Summary

Spring Testing provides comprehensive testing capabilities:

Key Features:

  • Test Context Framework: Manages Spring context for tests
  • Test Slices: Focused testing of specific layers
  • Mock Integration: Seamless mocking with @MockBean
  • Multiple Test Types: Unit, integration, and slice tests

Core Annotations:

  • @SpringBootTest: Full integration testing
  • @WebMvcTest: Web layer testing
  • @DataJpaTest: Repository testing
  • @JsonTest: JSON serialization testing

Best Practices:

  • Layer-Specific Testing: Use appropriate test slices
  • Test Data Management: Use builders and test data
  • Clear Test Structure: Organize with nested classes
  • Descriptive Names: Use meaningful test names
  • Mock External Dependencies: Isolate units under test

Testing Strategies:

  • Unit Tests: Fast, isolated component testing
  • Integration Tests: End-to-end functionality testing
  • Slice Tests: Focused layer testing
  • Security Tests: Authentication and authorization testing

Spring Testing enables building robust, well-tested applications with confidence in code quality and reliability.