테스트 표준
25.1. JUnit 5 테스트 작성 기준
25.1.1. 테스트 클래스 네이밍
| 대상 | 네이밍 규칙 | 예시 |
|---|---|---|
| 단위 테스트 | {대상클래스}Test | UserServiceTest |
| 통합 테스트 | {대상클래스}IntegrationTest | UserControllerIntegrationTest |
| 슬라이스 테스트 | {대상클래스}Test + 어노테이션으로 구분 | UserControllerTest (@WebMvcTest) |
25.1.2. 테스트 메서드 네이밍
한글 서술형 메서드명을 허용합니다. 테스트 의도가 명확하게 드러나야 합니다.
java
@Test
void 이메일로_사용자를_조회한다() {
// ...
}
@Test
void 존재하지_않는_사용자_조회_시_예외가_발생한다() {
// ...
}25.1.3. Given-When-Then 패턴
모든 테스트는 Given-When-Then 구조로 작성합니다.
java
@Test
void 이메일로_사용자를_조회한다() {
// given
String email = "user@tienipia.com";
User expected = new User(1L, "홍길동", email);
given(userRepository.findByEmail(email)).willReturn(Optional.of(expected));
// when
User result = userService.findByEmail(email);
// then
assertThat(result.getName()).isEqualTo("홍길동");
assertThat(result.getEmail()).isEqualTo(email);
}25.1.4. Assertion 라이브러리
- AssertJ를 표준으로 사용합니다.
- JUnit 5 기본
Assertions도 사용 가능하지만, AssertJ의 fluent API를 권장합니다.
java
// AssertJ (권장)
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("홍길동");
assertThat(users).hasSize(3).extracting("name").contains("홍길동");
// 예외 검증
assertThatThrownBy(() -> userService.findById(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessageContaining("999");25.2. 테스트 분류
25.2.1. 단위 테스트
- 단일 클래스 또는 메서드를 격리하여 테스트합니다.
- 외부 의존성은 Mockito로 모킹합니다.
- Spring 컨텍스트를 로드하지 않습니다.
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserServiceImpl userService;
@Test
void 사용자를_생성한다() {
// given
CreateUserRequest request = new CreateUserRequest("홍길동", "user@tienipia.com");
given(userRepository.insert(request.name(), request.email()))
.willReturn(UsersRecordFixture.create(1L, "홍길동", "user@tienipia.com"));
// when
UserResponse response = userService.createUser(request);
// then
assertThat(response.name()).isEqualTo("홍길동");
then(userRepository).should().insert(request.name(), request.email());
}
}25.2.2. 슬라이스 테스트
특정 레이어만 로드하여 테스트합니다.
| 어노테이션 | 대상 레이어 | 용도 |
|---|---|---|
@WebMvcTest | Controller | API 요청/응답 검증 |
@JooqTest | Repository | jOOQ 쿼리 검증 |
@JsonTest | JSON 직렬화 | DTO 변환 검증 |
java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void 사용자_목록을_조회한다() throws Exception {
given(userService.findAll())
.willReturn(List.of(new UserResponse(1L, "홍길동", "user@tienipia.com")));
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("홍길동"));
}
}25.2.3. Repository 테스트
jOOQ 기반 Repository 테스트는 @JooqTest 또는 @SpringBootTest + Testcontainers를 사용합니다.
@JooqTest 사용 예시:
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 사용자를_생성하고_조회한다() {
// given
userRepository.insert("홍길동", "user@tienipia.com");
// when
Optional<UsersRecord> result = userRepository.findByEmail("user@tienipia.com");
// then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("홍길동");
}
}- Testcontainers가 실제 PostgreSQL 컨테이너를 실행하므로, PostgreSQL 고유 기능(인덱스, 타입 등)을 정확히 테스트할 수 있습니다.
- Flyway 마이그레이션이 자동으로 적용되어 테스트 DB가 초기화됩니다.
25.2.4. 통합 테스트
@SpringBootTest로 전체 컨텍스트를 로드합니다.- 실제 데이터베이스 대신 Testcontainers PostgreSQL을 사용합니다. H2는 사용하지 않습니다.
- 통합 테스트는 CI에서 반드시 실행합니다.
25.3. 커버리지 기준
25.3.1. 최소 커버리지
| 측정 기준 | 최소 비율 |
|---|---|
| 라인 커버리지 | 80% 이상 |
| 브랜치 커버리지 | 70% 이상 |
25.3.2. 제외 대상
다음 항목은 커버리지 측정에서 제외합니다.
- Configuration 클래스 (
*Config.java) - DTO (단순 데이터 클래스)
- Main 클래스 (
*Application.java) - jOOQ 자동 생성 코드 (
jooq/패키지)
JaCoCo에서 제외 설정:
xml
<configuration>
<excludes>
<exclude>**/*Config.*</exclude>
<exclude>**/*Application.*</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/jooq/**</exclude>
</excludes>
</configuration>25.3.3. 커버리지 리포트
- JaCoCo가 HTML 리포트를
target/site/jacoco/에 생성합니다. - CI에서 리포트를 아티팩트로 저장하여 리뷰 시 참고합니다.
25.4. 테스트 데이터 관리
25.4.1. 테스트 픽스처
- 테스트 데이터 생성은 팩토리 메서드 또는 빌더 패턴을 사용합니다.
- 테스트 간 데이터 공유는 금지합니다. 각 테스트는 독립적으로 실행되어야 합니다.
java
class UserFixture {
public static User createDefaultUser() {
return new User(1L, "홍길동", "user@tienipia.com");
}
public static User createUser(String name, String email) {
return new User(null, name, email);
}
}25.4.2. 테스트 원칙
- 테스트는 독립적이어야 합니다. 실행 순서에 의존하지 않습니다.
- 테스트는 반복 가능해야 합니다. 외부 상태에 의존하지 않습니다.
- 테스트는 빠르게 실행되어야 합니다. 단위 테스트 전체가 30초 이내에 완료되는 것을 목표로 합니다.