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.