커뮤니티 웹 서비스 프로젝트 Infio에 테스트 코드를 도입하며 이런저런 생각을 하게 됐다. 잊기 전에 유의미한 테스트 코드에 초점을 맞춰 고민했던 흔적을 기록하고자 한다.
프로젝트 Infio에 테스트 코드를 도입할 때 세웠던 목표는 테스트 커버리지 100%였다.
거창한 이유가 있었던 건 아니고 토스의 테스트 커버리지 100%가 계기였다. 100%라는 숫자로 수많은 엣지 케이스를 예방했음을 증명해내는 게 멋있어 보였다. 그리고 토스에 비해 프로젝트 크기가 매우 작은 편인 내 프로젝트에 테스트 커버리지 100%를 적용하는 것은 쉬울 것 같았다.
하지만 100%를 목표로 테스트 코드를 작성하다 보니 테스트 내용보다 테스트 커버리지 자체에 집중하게 됐다.
@ExtendWith(MockitoExtension.class)
class ArticleControllerTest {
@Mock
private ArticleQueryService articleQueryService;
@InjectMocks
private ArticleController articleController;
@Test
void givenValidRequest_whenGetArticles_thenSucceeds() {
// given
int page = 1;
int size = 7;
GetArticlesResponseDto expectedDto = new GetArticlesResponseDto(
// 생략
);
ApiResponse<GetArticlesResponseDto> expected =
ApiResponse.onSuccess(SuccessCode.SUCCESS, expectedDto);
when(articleQueryService.getArticles(page, size)).thenReturn(expectedDto); // 예상하는 DTO를 반환해라
// when
ApiResponse<GetArticlesResponseDto> result = articleController.getArticles(page, size);
// then
assertThat(result.getData()).isEqualTo(expectedDto); // 예상하는 DTO를 반환했는가 검증???
}
}
이 테스트 코드는 Service가 DTO를 반환하도록 Mock으로 강제하고, 해당 DTO를 실제로 받는지만 확인한다. 테스트가 성공하든 실패하든 실제 API 계약, 도메인 로직, 보안 정책과는 아무 관련이 없는 테스트인 것이다.
물론 테스트 커버리지 100%를 달성하는 것도 중요하다. 하지만 숫자를 맞추기 위해 의미 없는 테스트를 추가하기보다는 실제 장애를 막고 테스트 코드의 목적을 뒷받침해줄 수 있는 테스트에 우선순위를 두기로 했다.
테스트 코드의 목적을 되새겨보면 유의미한 테스트 코드가 무엇인지는 쉽게 알 수 있다.
이 목적들을 달성하는 테스트 코드가 유의미한 테스트 코드일 것이고 이를 한 마디로 표현하자면...
좋은 테스트의 덕목을 갖춘, 비즈니스적으로 가치 있는 테스트
라고 할 수 있겠다.
세 가지 기준으로 판별해볼 수 있다.
테스트 코드는 한 번 작성하고 다시는 수정하지 않는 코드가 아니다. 기획과 요구사항이 변경됨에 따라 함께 수정해야 하는 코드 중 하나이다. 그러므로 코드 부채가 되지 않도록 유의미한 테스트 코드를 작성하는 게 중요하다.
테스트가 꼭 필요한 영역에 우선순위를 두어 테스트하기로 했다.
Presentation Layer (Controller/DTO)
비즈니스 로직 없이 라우팅과 단순 위임이 대부분이다. 따라서 필요 시에만 단위 테스트를 진행한다.
Spring Security를 검증하기 위해 통합 테스트에서 HTTP 계약(인증/인가)을 검증하였다.
Business Layer (Service/Domain)
도메인 규칙과 경계/예외가 있는 계층이다. 가장 높은 우선순위를 두어 테스트를 작성하며 가능한 100% 커버리지를 달성한다.
이 규칙을 지키며 작성한 테스트 코드의 예시를 하나씩 보자.
@ExtendWith(MockitoExtension.class)
public class UserValidatorTest {
@Mock
UserRepository userRepository;
@InjectMocks
UserValidator userValidator;
// ...
@Test
@DisplayName("이미 존재하는 이메일이면 EMAIL_ALREADY_EXISTS 예외 발생")
void givenDuplicateEmail_whenValidateEmailIsNotTaken_thenThrowsEmailAlreadyExistsException() {
// given
when(userRepository.existsByEmail("email@e.com")).thenReturn(true);
// when
GeneralException exc = assertThrows(
GeneralException.class,
() -> userValidator.validateEmailIsNotTaken("email@e.com")
);
// then
assertEquals(ErrorCode.EMAIL_ALREADY_EXISTS, exc.getErrorCode());
}
}
User가 존재하는지 검증하는 서비스 로직 UserValidator에 대해 UserValidatorTest를 작성했다. repository가 특정 email이 존재한다고 응답했을 때, 정확히 EMAIL_ALREADY_EXISTS 예외를 던지는지를 검증하는 테스트다.
이 과정을 거치며 findByEmail().isPresent()로 작성되어 있던 코드를
existsByEmail()로 바꾸는 리팩터링을 하게 되었다.
코드 한 줄을 미세하게 변경한 수준이지만, 테스트를 작성하는 과정에서 실제 코드 품질을 개선할 수 있다는 걸 직접 경험해볼 수 있었다.
@ParameterizedTest
@ValueSource(strings = {
"application/json",
"text/plain",
"application/pdf",
"application/x-msdownload",
"application/octet-stream"
})
@DisplayName("업로드 한 파일의 형태가 image~가 아닌 경우 FILE_FORMAT_ERROR를 받는다.")
void givenWrongFormatFile_whenUploadFile_thenThrowsFileFormatException(String contentType) {
// given
byte[] content = "test-json".getBytes();
MockMultipartFile mockFile = new MockMultipartFile(
"file",
"file.bin",
contentType,
content
);
// when
GeneralException exc = assertThrows(GeneralException.class,
() -> fileStorageService.store(mockFile, "test"));
//then
assertEquals(ErrorCode.FILE_FORMAT_ERROR, exc.getErrorCode());
}
이미지가 아닌 파일 업로드 시 FILE_FORMAT_ERROR를 반환하는지를 확인하는 테스트다.
실제로 이미지가 아닌 파일을 서버에 저장하게 될 경우, 보안 위험의 우려가 있어 테스트로 반드시 검증해야 한다고 생각하였다.
인증/인가 시나리오 통합 테스트를 작성할 때 실제 시나리오를 고민하며 작성했다. "로그인에 성공한다"는 정도만 확인해서는 부족하다고 생각하여 실제 사용자가 겪을 수 있는 보안 시나리오를 테스트로 옮기는 데 집중했다.
와 같은 흐름을 통합 테스트로 검증했다.
이 테스트들은 단위 테스트만으로는 잡기 어려운 여러 모듈(필터, 토큰 유틸, 인증 로직, 예외 핸들러)이 함께 동작하는 흐름을 다루기 때문에 의미가 있다고 생각했다.
필요하다고 생각하는 테스트 코드를 작성하고 난 뒤 테스트 커버리지와 테스트 개수는 다음과 같았다.

Instructions 기준 91%
Branches 기준 72%

81개 테스트 수행 소요 시간 약 3초
테스트 커버리지를 높이는 것을 목적으로 삼지 않았다고 커버리지 도구를 사용하지 않은 것은 아니다. 커버리지 도구로 필요한 비즈니스 로직에서 놓친 부분이 있는지 검증할 수 있었다.
결국, 테스트 코드 작성에서 중요한 것은 커버리지와 유의미한 테스트에 대한 기준을 섞어가며 서비스를 위험으로부터 보호할 수 있는 테스트 코드를 작성할 수 있는 것이라고 생각한다.
Viva Republica - 테스트 커버리지 100%
Viva Republica - 가치있는 테스트를 위한 전략과 구현
강남언니 공식 블로그 - [SaaS] 테스트 안정감을 N배로 확보할 수 있었던 이유
잘 읽었습니다~ 좋은 글 감사해요^^
Infio 는 어떤 프로젝트인지 궁금하네요 🤔