Spring Test

Complete guide to Spring Test: learn how to write unit tests and integration tests for Spring applications. Master Spring's testing framework, mocking, test context, and best practices for testing Spring components.

Table of Contents

1. What is Spring Test?

Spring Test is a comprehensive testing framework that provides support for unit testing and integration testing of Spring applications. It extends JUnit and TestNG with Spring-specific features, making it easy to test Spring components, services, repositories, and controllers.

Spring Test provides:

  • Dependency injection for test classes
  • Test context management and caching
  • Mocking support with Mockito integration
  • Transaction management for tests
  • Web testing support for MVC controllers
  • Test profiles and property sources
  • Database testing with test containers

The following diagram illustrates the Spring Test architecture:

graph TB subgraph "Test Framework" A[JUnit/TestNG] B[Spring Test] A --> B end subgraph "Spring Test Features" C[Test Context Framework] D[Dependency Injection] E[Mocking Support] F[Transaction Management] G[Web Test Support] end subgraph "Test Types" H[Unit Tests] I[Integration Tests] J[Web Tests] K[Repository Tests] end B --> C B --> D B --> E B --> F B --> G C --> H C --> I G --> J D --> K style A fill:#fff4e1,stroke:#f57c00,stroke-width:2px style B fill:#e1f5ff,stroke:#0273bd,stroke-width:3px style C fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style H fill:#fce4ec,stroke:#c2185b,stroke-width:2px style I fill:#fce4ec,stroke:#c2185b,stroke-width:2px

2. Why Use Spring Test?

  • Dependency Injection: Automatically inject Spring beans into test classes
  • Context Caching: Reuse application context across tests for faster execution
  • Transaction Management: Automatic rollback of database transactions in tests
  • Mocking Integration: Seamless integration with Mockito for mocking dependencies
  • Web Testing: Test MVC controllers without starting a web server
  • Profile Support: Use different Spring profiles for different test scenarios
  • Property Override: Override application properties for testing
  • Test Slices: Load only necessary parts of the application context

3. Testing Types

3.1 Unit Testing

Unit tests verify individual components in isolation, typically using mocks for dependencies. Spring Test makes unit testing easier by providing dependency injection and mocking support.

3.1.1 Unit Test Example

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testGetUser() {
        // Given
        User expectedUser = new User(1L, "John Doe");
        when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
        
        // When
        User result = userService.getUser(1L);
        
        // Then
        assertNotNull(result);
        assertEquals("John Doe", result.getName());
        verify(userRepository).findById(1L);
    }
    
    @Test
    void testCreateUser() {
        // Given
        User newUser = new User(null, "Jane Doe");
        User savedUser = new User(1L, "Jane Doe");
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        // When
        User result = userService.createUser(newUser);
        
        // Then
        assertNotNull(result.getId());
        assertEquals("Jane Doe", result.getName());
        verify(userRepository).save(newUser);
    }
}

3.1.2 Testing with Spring Context (Lightweight)

@SpringJUnitConfig(TestConfig.class)
class UserServiceSpringTest {
    @Autowired
    private UserService userService;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    void testGetUser() {
        User expectedUser = new User(1L, "John Doe");
        when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
        
        User result = userService.getUser(1L);
        
        assertEquals("John Doe", result.getName());
    }
}

3.2 Integration Testing

Integration tests verify that multiple components work together correctly. Spring Test provides full application context loading for integration tests.

The following diagram shows the integration testing flow:

graph LR subgraph "Test Execution" A[Test Class] --> B[Spring Test Context] B --> C[Load Application Context] C --> D[Inject Dependencies] end subgraph "Application Components" D --> E[Services] D --> F[Repositories] D --> G[Controllers] end subgraph "Test Database" F --> H[Test DataSource] H --> I[Test Database] end subgraph "Test Execution" E --> J[Execute Test] F --> J G --> J J --> K[Verify Results] K --> L[Rollback Transaction] end style A fill:#e1f5ff,stroke:#0273bd,stroke-width:2px style B fill:#fff4e1,stroke:#f57c00,stroke-width:2px style C fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style J fill:#fce4ec,stroke:#c2185b,stroke-width:2px

3.2.1 Full Integration Test

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Transactional
class UserServiceIntegrationTest {
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testCreateAndRetrieveUser() {
        // Given
        User newUser = new User(null, "John Doe", "john@example.com");
        
        // When
        User savedUser = userService.createUser(newUser);
        
        // Then
        assertNotNull(savedUser.getId());
        
        // Verify in database
        Optional<User> found = userRepository.findById(savedUser.getId());
        assertTrue(found.isPresent());
        assertEquals("John Doe", found.get().getName());
    }
    
    @Test
    void testUpdateUser() {
        // Given
        User user = userRepository.save(new User(null, "John", "john@example.com"));
        
        // When
        user.setName("John Updated");
        User updated = userService.updateUser(user);
        
        // Then
        assertEquals("John Updated", updated.getName());
        assertEquals("John Updated", userRepository.findById(user.getId()).get().getName());
    }
}

3.2.2 Repository Integration Test

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testFindByEmail() {
        // Given
        User user = new User(null, "John Doe", "john@example.com");
        userRepository.save(user);
        
        // When
        Optional<User> found = userRepository.findByEmail("john@example.com");
        
        // Then
        assertTrue(found.isPresent());
        assertEquals("John Doe", found.get().getName());
    }
    
    @Test
    void testFindByEmailNotFound() {
        // When
        Optional<User> found = userRepository.findByEmail("notfound@example.com");
        
        // Then
        assertFalse(found.isPresent());
    }
}

3.2.3 Web Layer Integration Test

@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testGetUser() throws Exception {
        // Given
        User user = new User(1L, "John Doe");
        when(userService.getUser(1L)).thenReturn(user);
        
        // When & Then
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.name").value("John Doe"));
    }
    
    @Test
    void testCreateUser() throws Exception {
        // Given
        User newUser = new User(null, "Jane Doe");
        User savedUser = new User(1L, "Jane Doe");
        when(userService.createUser(any(User.class))).thenReturn(savedUser);
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"Jane Doe\"}"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.name").value("Jane Doe"));
    }
}

4. Test Context Framework

The Spring Test Context Framework manages the application context for tests, providing dependency injection and context caching.

4.1 Test Context Annotations

// Load full Spring Boot application context
@SpringBootTest
class FullIntegrationTest {
    @Autowired
    private ApplicationContext context;
}

// Load only web layer (MVC)
@WebMvcTest(UserController.class)
class WebLayerTest {
    @Autowired
    private MockMvc mockMvc;
}

// Load only JPA repositories
@DataJpaTest
class RepositoryTest {
    @Autowired
    private UserRepository repository;
}

// Load only JSON support
@JsonTest
class JsonTest {
    @Autowired
    private JacksonTester<User> jsonTester;
}

// Load only JDBC support
@JdbcTest
class JdbcTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

// Custom configuration
@SpringJUnitConfig(TestConfig.class)
class CustomConfigTest {
    @Autowired
    private UserService userService;
}

4.2 Context Caching

Spring Test caches application contexts to improve test performance. Contexts are cached based on configuration.

// Same context configuration = cached context
@SpringBootTest
class Test1 {
    // Context loaded
}

@SpringBootTest
class Test2 {
    // Same context reused (cached)
}

@SpringBootTest(classes = DifferentConfig.class)
class Test3 {
    // New context loaded (different configuration)
}

4.3 Test Profiles

@SpringBootTest
@ActiveProfiles("test")
class ProfileTest {
    @Autowired
    private Environment environment;
    
    @Test
    void testProfileActive() {
        assertTrue(environment.acceptsProfiles(Profiles.of("test")));
    }
}

// Multiple profiles
@SpringBootTest
@ActiveProfiles({"test", "integration"})
class MultipleProfilesTest {
    // Test code
}

4.4 Test Property Sources

@SpringBootTest
@TestPropertySource(properties = {
    "app.name=TestApp",
    "app.version=1.0.0"
})
class PropertySourceTest {
    @Value("${app.name}")
    private String appName;
    
    @Test
    void testProperty() {
        assertEquals("TestApp", appName);
    }
}

// From file
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class PropertyFileTest {
    // Properties loaded from test.properties
}

5. Mocking with Spring

Spring Test integrates seamlessly with Mockito for mocking dependencies in tests.

5.1 @MockBean and @SpyBean

@SpringBootTest
class MockingTest {
    // Mock a bean (replaces real bean in context)
    @MockBean
    private UserRepository userRepository;
    
    // Spy on a bean (wraps real bean, allows partial mocking)
    @SpyBean
    private EmailService emailService;
    
    @Autowired
    private UserService userService;
    
    @Test
    void testWithMock() {
        // Given
        User user = new User(1L, "John");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        // When
        User result = userService.getUser(1L);
        
        // Then
        assertEquals("John", result.getName());
        verify(userRepository).findById(1L);
    }
    
    @Test
    void testWithSpy() {
        // Real method called, but can verify
        userService.sendWelcomeEmail(1L);
        
        // Verify real method was called
        verify(emailService).sendEmail(anyString());
    }
}

5.2 Mockito with Spring

@ExtendWith(MockitoExtension.class)
class MockitoTest {
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testMocking() {
        // Setup mocks
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(new User(1L, "John")));
        doNothing().when(emailService).sendEmail(anyString());
        
        // Execute
        User user = userService.getUser(1L);
        userService.sendWelcomeEmail(user.getId());
        
        // Verify
        verify(userRepository).findById(1L);
        verify(emailService).sendEmail(anyString());
    }
}

5.3 Argument Matchers

@Test
void testArgumentMatchers() {
    // Any argument
    when(userRepository.save(any(User.class)))
        .thenReturn(new User(1L, "John"));
    
    // Specific argument
    when(userRepository.findById(eq(1L)))
        .thenReturn(Optional.of(new User(1L, "John")));
    
    // Argument captor
    ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
    userService.createUser(new User(null, "John"));
    
    verify(userRepository).save(captor.capture());
    User captured = captor.getValue();
    assertEquals("John", captured.getName());
}

6. Real-World Examples

6.1 Example 1: Service Layer Test

@SpringBootTest
@Transactional
class OrderServiceIntegrationTest {
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @MockBean
    private PaymentService paymentService;
    
    @MockBean
    private EmailService emailService;
    
    @Test
    void testCreateOrder() {
        // Given
        Order order = new Order();
        order.setItems(Arrays.asList(new OrderItem("Product1", 10.0)));
        
        when(paymentService.processPayment(any(BigDecimal.class)))
            .thenReturn(new PaymentResult(true, "Success"));
        doNothing().when(emailService).sendOrderConfirmation(any(Order.class));
        
        // When
        Order created = orderService.createOrder(order);
        
        // Then
        assertNotNull(created.getId());
        assertEquals(OrderStatus.CONFIRMED, created.getStatus());
        verify(paymentService).processPayment(any(BigDecimal.class));
        verify(emailService).sendOrderConfirmation(created);
    }
}

6.2 Example 2: REST Controller Test

@WebMvcTest(UserController.class)
class UserControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void testGetAllUsers() throws Exception {
        // Given
        List<User> users = Arrays.asList(
            new User(1L, "John"),
            new User(2L, "Jane")
        );
        when(userService.getAllUsers()).thenReturn(users);
        
        // When & Then
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(2)))
            .andExpect(jsonPath("$[0].name").value("John"))
            .andExpect(jsonPath("$[1].name").value("Jane"));
    }
    
    @Test
    void testCreateUser() throws Exception {
        // Given
        UserDto userDto = new UserDto("John Doe");
        User savedUser = new User(1L, "John Doe");
        when(userService.createUser(any(User.class))).thenReturn(savedUser);
        
        // When & Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userDto)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.name").value("John Doe"));
    }
}

6.3 Example 3: Repository Test with Test Containers

@SpringBootTest
@Testcontainers
@Transactional
class UserRepositoryIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @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);
    }
    
    @Test
    void testSaveAndFind() {
        // Given
        User user = new User(null, "John Doe", "john@example.com");
        
        // When
        User saved = userRepository.save(user);
        Optional<User> found = userRepository.findById(saved.getId());
        
        // Then
        assertTrue(found.isPresent());
        assertEquals("John Doe", found.get().getName());
    }
}

6.4 Example 4: Testing with Transactions

@SpringBootTest
@Transactional
class TransactionalTest {
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testTransactionRollback() {
        // This test will rollback automatically
        User user = userService.createUser(new User(null, "Test"));
        
        // Verify in same transaction
        assertTrue(userRepository.existsById(user.getId()));
    }
    
    @Test
    @Rollback(false) // Commit transaction
    void testTransactionCommit() {
        User user = userService.createUser(new User(null, "Test"));
        
        // Transaction committed, data persists
        assertTrue(userRepository.existsById(user.getId()));
    }
    
    @Test
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    void testWithoutTransaction() {
        // No transaction, manual cleanup required
        User user = userService.createUser(new User(null, "Test"));
        // Cleanup manually if needed
    }
}

7. Best Practices

7.1 Use Appropriate Test Slices

Use test slices to load only what you need:

// For repository tests
@DataJpaTest  // Only loads JPA components

// For web layer tests
@WebMvcTest(Controller.class)  // Only loads MVC components

// For full integration tests
@SpringBootTest  // Loads full application context

7.2 Keep Tests Independent

Each test should be independent and not rely on other tests:

@SpringBootTest
@Transactional
class IndependentTest {
    @Test
    void test1() {
        // This test doesn't depend on test2
    }
    
    @Test
    void test2() {
        // This test doesn't depend on test1
    }
}

7.3 Use @DirtiesContext Sparingly

Avoid @DirtiesContext unless necessary, as it prevents context caching:

// Avoid if possible
@DirtiesContext  // Forces context reload

// Better: Use @MockBean or test profiles
@MockBean
private SomeService someService;

7.4 Test Naming Conventions

// Good naming
@Test
void shouldReturnUserWhenIdExists() {
    // Test implementation
}

@Test
void shouldThrowExceptionWhenUserNotFound() {
    // Test implementation
}

// Avoid vague names
@Test
void test1() {  // Bad
    // Test implementation
}

7.5 Arrange-Act-Assert Pattern

@Test
void testCreateUser() {
    // Arrange (Given)
    User newUser = new User(null, "John");
    when(userRepository.save(any())).thenReturn(new User(1L, "John"));
    
    // Act (When)
    User result = userService.createUser(newUser);
    
    // Assert (Then)
    assertNotNull(result.getId());
    assertEquals("John", result.getName());
}

8. Advanced Concepts

8.1 Custom Test Configuration

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

@SpringBootTest
@Import(TestConfig.class)
class CustomConfigTest {
    // Uses test configuration
}

8.2 Test Execution Listeners

@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class
})
@SpringBootTest
class CustomListenerTest {
    // Custom test execution listeners
}

8.3 Parallel Test Execution

// In application.properties
spring.test.context.cache.maxSize=32
spring.test.context.cache.maxAge=3600

// Tests can run in parallel with proper isolation
@SpringBootTest
class ParallelTest {
    // Context cached and reused across parallel tests
}

8.4 Testing with Testcontainers

@SpringBootTest
@Testcontainers
class TestcontainersTest {
    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7")
            .withExposedPorts(6379);
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.host", redis::getHost);
        registry.add("spring.redis.port", redis::getFirstMappedPort);
    }
    
    @Test
    void testWithRedis() {
        // Test with real Redis container
    }
}

9. Conclusion

Spring Test provides a comprehensive testing framework for Spring applications, supporting both unit and integration testing. With features like dependency injection, context caching, mocking support, and transaction management, Spring Test makes it easy to write effective tests for Spring applications.

By understanding test slices, mocking strategies, and best practices, you can write maintainable and efficient tests that verify your application's functionality. Whether you're writing unit tests with mocks or full integration tests with real components, Spring Test provides the tools you need.

Remember to use appropriate test slices, keep tests independent, and follow the Arrange-Act-Assert pattern for clear and maintainable test code. With Spring Test, you can confidently test your Spring applications and ensure they work correctly.

Post a Comment

0 Comments