Testing Standards
25.1. JUnit 5 Test Writing Criteria
25.1.1. Test Class Naming
| Target | Naming Convention | Example |
|---|---|---|
| Unit Test | {TargetClass}Test | UserServiceTest |
| Integration Test | {TargetClass}IntegrationTest | UserControllerIntegrationTest |
| Slice Test | {TargetClass}Test + distinguished by annotation | UserControllerTest (@WebMvcTest) |
25.1.2. Test Method Naming
Descriptive method names in the project's primary language are permitted. The test intent must be clearly expressed.
java
@Test
void findsUserByEmail() {
// ...
}
@Test
void throwsExceptionWhenUserNotFound() {
// ...
}25.1.3. Given-When-Then Pattern
All tests must be written using the Given-When-Then structure.
java
@Test
void findsUserByEmail() {
// given
String email = "user@tienipia.com";
User expected = new User(1L, "John Doe", email);
given(userRepository.findByEmail(email)).willReturn(Optional.of(expected));
// when
User result = userService.findByEmail(email);
// then
assertThat(result.getName()).isEqualTo("John Doe");
assertThat(result.getEmail()).isEqualTo(email);
}25.1.4. Assertion Library
- AssertJ must be used as the standard.
- JUnit 5's built-in
Assertionsmay also be used, but AssertJ's fluent API is recommended.
java
// AssertJ (recommended)
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("John Doe");
assertThat(users).hasSize(3).extracting("name").contains("John Doe");
// Exception verification
assertThatThrownBy(() -> userService.findById(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");25.2. Test Classification
25.2.1. Unit Tests
- A single class or method must be tested in isolation.
- External dependencies must be mocked using Mockito.
- The Spring context must not be loaded.
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserServiceImpl userService;
@Test
void createsUser() {
// given
CreateUserRequest request = new CreateUserRequest("John Doe", "user@tienipia.com");
given(userRepository.insert(request.name(), request.email()))
.willReturn(UsersRecordFixture.create(1L, "John Doe", "user@tienipia.com"));
// when
UserResponse response = userService.createUser(request);
// then
assertThat(response.name()).isEqualTo("John Doe");
then(userRepository).should().insert(request.name(), request.email());
}
}25.2.2. Slice Tests
Slice tests load only specific layers for testing.
| Annotation | Target Layer | Purpose |
|---|---|---|
@WebMvcTest | Controller | API request/response verification |
@JooqTest | Repository | jOOQ query verification |
@JsonTest | JSON Serialization | DTO conversion verification |
java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void retrievesUserList() throws Exception {
given(userService.findAll())
.willReturn(List.of(new UserResponse(1L, "John Doe", "user@tienipia.com")));
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("John Doe"));
}
}25.2.3. Repository Tests
jOOQ-based Repository tests must use @JooqTest or @SpringBootTest + Testcontainers.
@JooqTest Usage Example:
java
@JooqTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("flowin_test")
.withUsername("test")
.withPassword("test");
@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);
}
@Autowired
private DSLContext dsl;
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository = new UserRepository(dsl);
}
@Test
void createsAndRetrievesUser() {
// given
userRepository.insert("John Doe", "user@tienipia.com");
// when
Optional<UsersRecord> result = userRepository.findByEmail("user@tienipia.com");
// then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("John Doe");
}
}- Testcontainers runs an actual PostgreSQL container, enabling accurate testing of PostgreSQL-specific features (indexes, types, etc.).
- Flyway migrations are automatically applied to initialize the test database.
25.2.4. Integration Tests
@SpringBootTestmust be used to load the full context.- Testcontainers PostgreSQL must be used instead of an actual database. H2 must not be used.
- Integration tests must be executed in CI.
25.3. Coverage Criteria
25.3.1. Minimum Coverage
| Metric | Minimum Threshold |
|---|---|
| Line Coverage | 80% or higher |
| Branch Coverage | 70% or higher |
25.3.2. Exclusions
The following items must be excluded from coverage measurement.
- Configuration classes (
*Config.java) - DTOs (simple data classes)
- Main classes (
*Application.java) - jOOQ auto-generated code (
jooq/package)
JaCoCo exclusion configuration:
xml
<configuration>
<excludes>
<exclude>**/*Config.*</exclude>
<exclude>**/*Application.*</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/jooq/**</exclude>
</excludes>
</configuration>25.3.3. Coverage Reports
- JaCoCo generates HTML reports in
target/site/jacoco/. - Reports must be stored as artifacts in CI for reference during reviews.
25.4. Test Data Management
25.4.1. Test Fixtures
- Test data creation must use factory methods or the builder pattern.
- Sharing data between tests is prohibited. Each test must execute independently.
java
class UserFixture {
public static User createDefaultUser() {
return new User(1L, "John Doe", "user@tienipia.com");
}
public static User createUser(String name, String email) {
return new User(null, name, email);
}
}25.4.2. Test Principles
- Tests must be independent. They must not depend on execution order.
- Tests must be repeatable. They must not depend on external state.
- Tests must execute quickly. The goal is for all unit tests to complete within 30 seconds.