This document provides comprehensive guidance for testing within the Spring User Framework project.
- Testing Strategy
- Test Categories
- Custom Test Annotations
- Test Execution
- Test Data Management
- Best Practices
- Common Patterns
- Troubleshooting
Our testing approach follows a multi-layered strategy:
- Unit Tests - Fast, isolated tests for individual components
- Integration Tests - Tests that verify component interactions
- API Tests - End-to-end tests for REST endpoints
- Security Tests - Authentication and authorization verification
- Service Layer: Comprehensive unit testing with mocking
- Controller Layer: API testing with MockMvc
- Security Layer: Authentication and authorization scenarios
- Data Layer: Repository testing with test databases
The project uses custom annotations to categorize and configure tests:
- Purpose: Unit tests for service layer components
- Configuration:
- Mockito extension enabled
- Test profile activated
- Base test configuration imported
- Usage: Fast unit tests with mocked dependencies
@ServiceTest
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
// Test methods
}- Purpose: Full Spring context integration tests
- Configuration:
- Complete Spring Boot test context
- MockMvc configuration
- Transaction management
- All test configurations imported
- Usage: End-to-end workflow testing
@IntegrationTest
class UserRegistrationIntegrationTest {
@Autowired
private MockMvc mockMvc;
// Integration test methods
}- Purpose: Security-focused testing
- Configuration: Security test configuration
- Usage: Authentication/authorization testing
- Purpose: Repository and data layer testing
- Configuration: Database test configuration
- Usage: JPA repository testing
- Purpose: OAuth2 integration testing
- Configuration: OAuth2 mock configuration
- Usage: Social login testing
./gradlew test./gradlew testJdk17 # Run with JDK 17
./gradlew testJdk21 # Run with JDK 21
./gradlew testAll # Run with both JDK versions# Run only unit tests (ServiceTest)
./gradlew test --tests "*ServiceTest"
# Run only integration tests
./gradlew test --tests "*IntegrationTest"
# Run specific test class
./gradlew test --tests "UserServiceTest"Tests are configured to run in parallel for improved performance:
- Enabled: JUnit 5 parallel execution
- Strategy: Dynamic thread allocation based on CPU cores
- Configuration:
junit-platform.properties
# Parallel execution enabled
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
# Dynamic thread allocation
junit.jupiter.execution.parallel.config.strategy=dynamic- Unit Tests: ~30-60 seconds
- Integration Tests: ~2-5 minutes
- All Tests: ~5-10 minutes (with parallelization)
Use JUnit 5 tags for selective test execution:
@Test
@Tag("fast")
void quickTest() { /* ... */ }
@Test
@Tag("slow")
@Tag("integration")
void comprehensiveTest() { /* ... */ }# Run only fast tests
./gradlew test --tests "*" -Dgroups="fast"
# Exclude slow tests
./gradlew test --tests "*" -DexcludedGroups="slow"The centralized TestFixtures class provides consistent test data:
// Standard test entities
User user = TestFixtures.Users.standardUser();
User admin = TestFixtures.Users.adminUser();
User locked = TestFixtures.Users.lockedUser();
// DTOs for API testing
UserDto registration = TestFixtures.DTOs.validUserRegistration();
PasswordDto passwordUpdate = TestFixtures.DTOs.validPasswordUpdate();
// OAuth2 test users
OAuth2User googleUser = TestFixtures.OAuth2.googleUser();
OAuth2User githubUser = TestFixtures.OAuth2.githubUser();
// Security contexts
DSUserDetails userDetails = TestFixtures.Security.standardUserDetails();
// Test scenarios
TestFixtures.Scenarios.UserRegistration scenario = new TestFixtures.Scenarios.UserRegistration();For custom test data, use the fluent builders:
User customUser = UserTestDataBuilder.aUser()
.withEmail("custom@test.com")
.withFirstName("Custom")
.withLastName("User")
.withRole("ROLE_ADMIN")
.verified()
.build();
Role customRole = RoleTestDataBuilder.aRole()
.withName("ROLE_CUSTOM")
.withPrivilege("CUSTOM_PRIVILEGE")
.build();- Unit Tests: Use mocked repositories
- Integration Tests: Transactional rollback ensures clean state
- Test Isolation: Each test starts with a clean database state
Follow the Arrange-Act-Assert pattern:
@Test
void shouldUpdateUserProfile() {
// Arrange (Given)
User user = TestFixtures.Users.standardUser();
UserDto updateDto = TestFixtures.DTOs.profileUpdate();
when(userRepository.save(any())).thenReturn(user);
// Act (When)
User updatedUser = userService.updateProfile(user, updateDto);
// Assert (Then)
assertThat(updatedUser.getFirstName()).isEqualTo("Updated");
verify(userRepository).save(user);
}- Test Classes:
[ComponentName]Test(e.g.,UserServiceTest) - Test Methods:
should[ExpectedBehavior]When[Condition]or[methodName]_[condition]_[expectedResult] - Nested Classes:
@DisplayNamewith descriptive names
Use AssertJ for fluent assertions:
// Good
assertThat(user.getEmail()).isEqualTo("test@example.com");
assertThat(users).hasSize(3)
.extracting(User::getEmail)
.containsExactly("user1@test.com", "user2@test.com", "user3@test.com");
// Avoid
assertEquals("test@example.com", user.getEmail());Use ArgumentCaptors for complex verification:
@Test
void shouldPublishAuditEvent() {
// Act
userService.registerUser(userDto);
// Assert
ArgumentCaptor<AuditEvent> eventCaptor = ArgumentCaptor.forClass(AuditEvent.class);
verify(eventPublisher).publishEvent(eventCaptor.capture());
AuditEvent event = eventCaptor.getValue();
assertThat(event.getAction()).isEqualTo("Registration");
assertThat(event.getActionStatus()).isEqualTo("Success");
}@Test
void shouldAuthenticateWithOAuth2() throws Exception {
mockMvc.perform(post("/api/secure-endpoint")
.with(oauth2Login().oauth2User(TestFixtures.OAuth2.googleUser())))
.andExpect(status().isOk());
}@Test
void shouldRequireAuthentication() throws Exception {
mockMvc.perform(get("/user/profile"))
.andExpect(status().isUnauthorized());
}
@Test
void shouldAllowAccessForValidUser() throws Exception {
mockMvc.perform(get("/user/profile")
.with(user(TestFixtures.Security.standardUserDetails())))
.andExpect(status().isOk());
}@Test
void shouldSendVerificationEmail() {
// Act
userEmailService.sendRegistrationVerificationEmail(user, appUrl);
// Assert
ArgumentCaptor<Map<String, Object>> variablesCaptor = ArgumentCaptor.forClass(Map.class);
verify(mailService).sendTemplateMessage(
eq(user.getEmail()),
eq("Registration Confirmation"),
variablesCaptor.capture(),
eq("mail/registration-token.html")
);
Map<String, Object> variables = variablesCaptor.getValue();
assertThat(variables).containsKeys("token", "user", "confirmationUrl");
}@Test
void shouldThrowExceptionForDuplicateEmail() {
// Given
when(userRepository.findByEmail("test@example.com")).thenReturn(existingUser);
// When & Then
assertThatThrownBy(() -> userService.registerUser(userDto))
.isInstanceOf(UserAlreadyExistException.class)
.hasMessageContaining("email address");
}Problem: OAuth2 tests failing with dependency issues
Solution: Ensure @IntegrationTest annotation is used and Spring Security is properly configured
Problem: Tests failing when run in parallel Solution:
- Check for shared state between tests
- Use
@Tag("sequential")for tests that must run sequentially - Ensure proper test isolation
Problem: Security context not properly configured Solution:
- Use
@SecurityTestannotation - Apply
springSecurity()to MockMvc setup - Use
@WithMockUseroroauth2Login()for authentication
Problem: Tests affecting each other's database state Solution:
- Ensure
@Transactionalis applied to integration tests - Use
@Sqlannotations for specific test data setup - Check test isolation with
@DirtiesContextif needed
- Use Unit Tests: Prefer fast unit tests over integration tests
- Parallel Execution: Ensure tests are thread-safe for parallel execution
- Test Data: Use lightweight test fixtures instead of full database setup
- Mocking: Mock external dependencies to avoid network calls
# Run with debug logging
./gradlew test -Dlogging.level.org.springframework.security=DEBUG
# Run single test with full output
./gradlew test --tests "UserServiceTest.shouldRegisterUser" --info
# Debug test execution
./gradlew test --debug-jvmThis testing framework provides comprehensive coverage while maintaining fast execution times. The combination of custom annotations, centralized fixtures, and parallel execution ensures both developer productivity and code quality.
For questions or improvements to this testing strategy, please refer to the project documentation or create an issue in the repository.