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:
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:
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
}
}
0 Comments