The User Management API follows a comprehensive testing strategy with unit tests, integration tests, and code coverage reporting. The project maintains 80% code coverage as a quality standard.
Testing Overview
The application uses a layered testing approach that aligns with its hexagonal architecture:
Unit Tests Test business logic in isolation using Mockito
Integration Tests Test REST endpoints with MockMvc
Coverage Reports Track coverage with Jacoco
Testing Stack
The project uses the following testing technologies:
JUnit 5 - Testing framework
Mockito - Mocking framework
AssertJ - Fluent assertions
Spring Test - Spring-specific testing support
MockMvc - Testing REST controllers
Testcontainers - Integration testing with real databases
H2 - In-memory database for tests
Jacoco - Code coverage reporting
Running Tests
Run All Tests
Gradle (Linux/macOS)
Gradle (Windows)
With Coverage Report
The test task is configured in build.gradle:
test {
systemProperty 'spring.profiles.active', 'test'
useJUnitPlatform()
finalizedBy jacocoTestReport
}
Tests automatically use the test profile and generate a Jacoco coverage report after completion.
Run Specific Test Classes
./gradlew test --tests "*UserRestAdapterTest"
Run Specific Test Methods
./gradlew test --tests "*UserRestAdapterTest.createUser_success"
Run Tests in IDE
IntelliJ IDEA
Eclipse
VS Code
Right-click on test file or method
Select “Run ‘TestName’” or press Ctrl+Shift+F10 (Windows/Linux) or Cmd+Shift+R (macOS)
View results in the Run tool window
Right-click on test file
Select “Run As > JUnit Test”
View results in the JUnit view
Install Java Test Runner extension
Click the play button next to test methods
View results in the Test Explorer
Test Structure
Tests are organized in the same package structure as the source code:
src/test/java/com/fbaron/user/
├── core/
│ ├── model/
│ │ └── UserTest.java
│ └── usecase/
│ ├── RegisterUserUseCaseTest.java
│ ├── FindUserByIdUseCaseTest.java
│ ├── FindAllUsersUseCaseTest.java
│ ├── EditUserUseCaseTest.java
│ └── RemoveUserUseCaseTest.java
├── data/
│ └── jpa/
│ └── UserJpaAdapterTest.java
└── web/
└── rest/
└── UserRestAdapterTest.java
Test files follow the naming convention {ClassName}Test.java and are located in the same package as the class they test.
Unit Testing
Unit tests focus on the core business logic without Spring context.
Example: Use Case Test
Here’s an example of testing the RegisterUserUseCase:
RegisterUserUseCaseTest.java
@ ExtendWith ( MockitoExtension . class )
public class RegisterUserUseCaseTest {
@ Mock
private UserQueryRepository userQueryRepository ;
@ Mock
private UserCommandRepository userCommandRepository ;
private UserService underTest ;
@ BeforeEach
void setUp () {
underTest = new UserService (userQueryRepository, userCommandRepository);
}
@ Test
@ DisplayName ( "should register user when username and email are unique" )
void register_success () {
// Given
given ( userQueryRepository . existsByUsername ( "jdoe" )). willReturn ( false );
given ( userQueryRepository . existsByEmail ( "[email protected] " )). willReturn ( false );
given ( userCommandRepository . save ( any ( User . class ))). willReturn (savedUser);
var userToRegister = buildUser ( null );
// When
User result = underTest . register (userToRegister);
// Then
assertThat ( result . getId ()). isEqualTo (userId);
assertThat ( result . getUsername ()). isEqualTo ( "jdoe" );
then (userCommandRepository). should (). save ( any ( User . class ));
}
@ Test
@ DisplayName ( "should throw UserAlreadyExistsException when username is taken" )
void register_duplicateUsername_throws () {
// Given
given ( userQueryRepository . existsByUsername ( "jdoe" )). willReturn ( true );
var userToRegister = buildUser ( null );
// When & Then
assertThatThrownBy (() -> underTest . register (userToRegister))
. isInstanceOf ( UserAlreadyExistsException . class )
. hasMessageContaining ( "username" );
then (userCommandRepository). should ( never ()). save ( any ());
}
}
Key Testing Patterns
Use Mockito for Dependencies
Mock external dependencies with @Mock annotation: @ Mock
private UserQueryRepository userQueryRepository ;
Use BDD Style with Given-When-Then
Structure tests using BDD style: given ( repository . existsByUsername ( "jdoe" )). willReturn ( false );
User result = service . register (user);
then (repository). should (). save ( any ( User . class ));
Use AssertJ for Fluent Assertions
Write readable assertions: assertThat ( result . getUsername ()). isEqualTo ( "jdoe" );
assertThatThrownBy (() -> service . register (user))
. isInstanceOf ( UserAlreadyExistsException . class );
Use @DisplayName for Clarity
Add descriptive test names: @ DisplayName ( "should register user when username and email are unique" )
void register_success () { ... }
Integration Testing
Integration tests use @WebMvcTest to test REST controllers with the web layer.
Example: REST Adapter Test
@ WebMvcTest ( UserRestAdapter . class )
@ Import ({ UserDtoMapperImpl . class , GlobalExceptionHandler . class })
class UserRestAdapterTest {
@ Autowired
private MockMvc mockMvc ;
@ Autowired
private ObjectMapper objectMapper ;
@ MockitoBean
private RegisterUserUseCase registerUserUseCase ;
@ Test
@ DisplayName ( "POST /api/v1/users — should return 201 with created user" )
void createUser_success () throws Exception {
var user = buildDomainModelUser ( "jdoe" , "[email protected] " );
given ( registerUserUseCase . register ( any ())). willReturn (user);
RegisterUserDto request = new RegisterUserDto (
"jdoe" , "[email protected] " , "John" , "Doe" , UserRole . USER
);
mockMvc . perform ( post ( "/api/v1/users" )
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isCreated ())
. andExpect ( jsonPath ( "$.id" ). value ( userId . toString ()))
. andExpect ( jsonPath ( "$.username" ). value ( "jdoe" ))
. andExpect ( jsonPath ( "$.email" ). value ( "[email protected] " ));
}
@ Test
@ DisplayName ( "POST /api/v1/users — should return 400 when username is blank" )
void createUser_invalidRequest_returns400 () throws Exception {
RegisterUserDto request = new RegisterUserDto (
"" , "[email protected] " , "John" , "Doe" , UserRole . USER
);
mockMvc . perform ( post ( "/api/v1/users" )
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isBadRequest ())
. andExpect ( jsonPath ( "$.title" ). value ( "Validation Failed" ));
}
}
@WebMvcTest creates a lightweight Spring context with only the web layer, making tests fast and focused.
MockMvc Test Patterns
POST Request
GET Request
PUT Request
DELETE Request
mockMvc . perform ( post ( "/api/v1/users" )
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isCreated ())
. andExpect ( jsonPath ( "$.id" ). exists ());
mockMvc . perform ( get ( "/api/v1/users/{id}" , userId))
. andExpect ( status (). isOk ())
. andExpect ( jsonPath ( "$.username" ). value ( "jdoe" ));
mockMvc . perform ( put ( "/api/v1/users/{id}" , userId)
. contentType ( MediaType . APPLICATION_JSON )
. content ( objectMapper . writeValueAsString (request)))
. andExpect ( status (). isOk ());
mockMvc . perform ( delete ( "/api/v1/users/{id}" , userId))
. andExpect ( status (). isNoContent ());
Code Coverage with Jacoco
The project uses Jacoco for code coverage analysis.
Configuration
jacoco {
toolVersion = "0.8.12"
reportsDirectory = layout.buildDirectory.dir('reports/jacoco/test/html/')
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = false
html.required = true
html.outputLocation = layout.buildDirectory.dir('reports/tests/test/')
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"**/*mapper/**",
"**/*dto/**",
"**/*Entity/**",
"**/*Config.class"
])
}))
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80 // 80% coverage required
}
}
}
}
DTOs, mappers, entities, and configuration classes are excluded from coverage as they contain minimal logic.
Generating Coverage Reports
Run Tests with Coverage
./gradlew test jacocoTestReport
This runs all tests and generates the coverage report.
View the Report
Open the HTML report in your browser: open build/reports/tests/test/index.html
Or on Linux: xdg-open build/reports/tests/test/index.html
Check Coverage Percentage
The report shows:
Overall coverage percentage
Coverage by package
Coverage by class
Line-by-line coverage highlighting
Coverage Report Location
HTML Report : build/reports/tests/test/index.html
XML Report : build/reports/jacoco/test/jacocoTestReport.xml
The project enforces a minimum of 80% code coverage . The build will fail if coverage drops below this threshold.
Writing Effective Tests
Test Naming Convention
Use descriptive names that explain what is being tested:
@ Test
@ DisplayName ( "should throw UserNotFoundException when user does not exist" )
void getUserById_notFound_throwsException () {
// Test implementation
}
Arrange-Act-Assert Pattern
Structure tests with clear sections:
@ Test
void testExample () {
// Arrange (Given)
User user = User . builder (). username ( "jdoe" ). build ();
given ( repository . findById (userId)). willReturn ( Optional . of (user));
// Act (When)
User result = service . getUserById (userId);
// Assert (Then)
assertThat ( result . getUsername ()). isEqualTo ( "jdoe" );
}
Test Edge Cases
Test not only the happy path but also:
Invalid input
Null values
Empty collections
Duplicate data
Not found scenarios
Concurrent operations
Use Test Fixtures
Create reusable test data builders:
private User buildUser ( String username, String email) {
return User . builder ()
. id ( UUID . randomUUID ())
. username (username)
. email (email)
. firstName ( "John" )
. lastName ( "Doe" )
. role ( UserRole . USER )
. createdAt ( LocalDateTime . now ())
. updatedAt ( LocalDateTime . now ())
. active ( true )
. build ();
}
Continuous Integration Testing
The project includes CI/CD configuration for automated testing:
steps :
# Run Gradle Tests
- name : 'gradle:8.5-jdk21'
entrypoint : 'gradle'
args : [ 'test' ]
env :
- 'DOCKER_HOST=unix:///remote/docker.sock'
- 'TESTCONTAINERS_RYUK_DISABLED=true'
Tests run automatically on every push to the repository using Google Cloud Build.
Best Practices
Test in Isolation Unit tests should not depend on external services or databases
Fast Execution Tests should run quickly to enable rapid feedback
Independent Tests Tests should be able to run in any order
Clear Assertions Use descriptive assertions that clearly show what failed
Mock External Dependencies Use mocks for repositories, external APIs, and services
Test Behavior, Not Implementation Focus on what the code does, not how it does it
Troubleshooting
Tests fail with database connection error
Problem : Tests cannot connect to the database.Solution : Tests should use the test profile with H2 in-memory database. Ensure spring.profiles.active is set to test in the test configuration.
Coverage report not generated
Problem : Jacoco report is missing.Solution : Run tests with the report task:./gradlew test jacocoTestReport
The report will be in build/reports/tests/test/index.html.
Specific test fails in CI but passes locally
Problem : Test passes on local machine but fails in CI.Solution : Common causes:
Time zone differences
Environment variables
File system case sensitivity
Random data generation
Use fixed test data and explicit time zones.
MockMvc returns 404 for valid endpoint
Problem : REST endpoint test returns 404.Solution : Ensure:
@WebMvcTest includes the correct controller
URL path in test matches controller mapping
Controller is properly annotated with @RestController
Build fails with 'coverage below minimum'
Problem : Jacoco reports coverage below 80%.Solution : Add more tests to increase coverage:./gradlew test jacocoTestReport
Open the report to see which classes need more tests.
Next Steps
Installation Set up your development environment
Configuration Learn about configuration options
Docker Setup Run tests in Docker
Architecture Understand the system design