들어가기 앞서
오늘은 단위 테스트에 대해 공부하려고 합니다. 이전에 회사를 다닐때 단위 테스트는 조금 논란의 대상이였습니다.
- 찬성 : 단위 테스트를 통해 사이드 이펙트를 사전에 검증할 수 있고, 서비스를 안정적으로 운영하는데 도움이 돼. 특히 다른 사람의 코드를 건드릴때 단위 테스트만 있어도 큰 도움이 돼.
- 반대 : 일정이 바쁜데 단위 테스트 작성할 시간이 어디 있어. 그리고 코드 커버리지 채우기 위한 비효율적인 작업만 하잖아. 그냥 대충해
실제로 이전 회사에서 단위 테스트를 정말 의미 있게 사용하는 팀은 크게 없었습니다. 또한 다들 회의적인 반응이 항상 있었습니다. 그러면 지금 까지 글을 읽고, 너가 다녔던 회사가 기술적으로 좋지 않은 회사를 다녀서 그랬던거 아니야? 라고 하실수 있지만, 그 당시 네카라쿠배 중 하나에 속하는 회사의 개발자 분들이 회사에 새로 합류하고 있던 상황이였습니다. 그 분들 조차 단위 테스트 작성을 크게 중요시 하지 않았습니다. 단위 테스트 작성을 등한시 하시지는 않았지만 코드 커버리지 채우기 위한 용도 이외에는 크게 신경 쓰지 않으셨습니다. 저도 이러한 상황 속에서 조금 혼란이 많았습니다. 하지만 한번 테스트 코드를 제대로 작성해 보고 난 뒤에는 생각이 조금 바뀌게 되었고, 지금은 테스트 코드 작성이 매우 중요하다고 생각합니다. 이번 포스팅에서는 단순히 테스트 코드 작성이 아닌 의미있는 좋은 테스트 코드 작성에 대해 조금 알아 보겠습니다.
나는 왜 테스트 코드가 중요하다고 생각하게 되었을까?
시간이 바쁘신 분들을 위해 테스트 코드의 중요성을 미리 나열한다면, 아래와 같습니다.
- 빠른 장애 원인 파악
- 자동화
- 서비스 안정화
신규 프로젝트로 공통 공고 플랫폼 구축을 담당한 적이 있었습니다. 신규 프로젝트인 만큼 새로운 기능이 빠르게 늘어나고 코드량도 빠르게 늘어나고 있었습니다. 또한 공통 플랫폼 이다 보니 저희가 만든 신규 기능을 다른 서비스(다른 개발팀)들에서 사용하게 되었습니다.
하지만 문제는 여기서 발생했습니다. 배포를 할때마다 제가 담당한 프로젝트와 연관이 있는 모든 서비스와 API를 계속 QA검증을 해야 한다는 것이였습니다. 많은 장애는 예상하지 못한 사이드 이펙트에서 발생 하기에, 배포 관련이 없는 기능들도 QA검수 받아야 했습니다. 그러면 저는 1번 배포에 3~4개 팀의 QA검수를 받아야 했고, 배포한 기능 말고도 관련된 기능들을 다 검수를 받아야 했습니다. 그러면 시간이 배포 검수에만 많은 시간이 걸리고 장애가 발생한다고 해도 쉽게 원인을 파악하지 못했습니다.
그리고 팀원이 휴가를 간 사이 팀원이 담당하던 코드에서 장애가 발생한다면, 누구든 빠른 조치를 해야 합니다. 과연 해당 업무에 대한 도메인 이해가 깊지 않은 팀원이 코드를 수정하고 자신있게 배포를 할 수 있을까요? 혹시나 내가 비즈니스 로직을 잘못 변경한 것은 아닌지? 사이드 이펙트는 없는지? 배포 과정에 불안감이 따를수 밖에 없습니다.
당시 저희 팀은 단위, 통합, 인수 테스트를 모두 다 작성하여 문제를 해결하였습니다. 특별한 사유가 없다면 모든 MR의 코드 커버리지가 100%가 되도록 작성하였습니다.
테스트 코드를 작성하고 얻은 이점은 다음과 같습니다.
- 자동화를 통해 배포과정에 테스트 코드가 실패하면 배포가 중단 됩니다. 이로 인해 1차적으로 휴먼 에러를 줄일수 있었습니다.
- 테스트 코드 실패를 통해서 어디 부분에서 오류가 발생했는지 빠르게 파악할 수 있었습니다.
- 더 이상 과도한 QA검수를 진행하지 않습니다. 이로 인해 배포 이후 검수 과정이 단축 되고 검수에 필요로 하는 인원도 줄었습니다. 또한 사람이 검수하는 것 보다 빠르고 정확합니다.
- 내가 잘 알지 못하는 코드도 과감하게 수정할 수 있습니다. 테스트 코드가 잘 작성 되어 있다면, 저의 실수와 사이드 이펙트를 테스트 코드를 통해 확인 받을수 있습니다.
- 빠르게 변화하는 요구사항 속에서도 안정성을 보장할 수 있다.
- 테스트 코드는 프로덕션 기능을 설명하는 문서 이다. 테스트 코드를 통해 프로덕션 코드를 이해할 수 있다. 또한 팀의 전체적인 코드 품질을 향상 시킬수 있는 팀의 공유 자산이다.
테스트 코드의 이점은 단순히 테스트 코드가 작성되어 있다고 얻을수 있는 것이 아닌, 잘 짜여진 테스트 코드여야 얻을 수 있습니다.
내가 생각하는 좋은 테스트 코드를 작성하는 방법
1. 서비스 운영 중에 장애가 발생 했다면, 반드시 테스트 코드를 추가해서 재발을 방지하자.
저는 테스트 코드 작성에 있어 이 부분이 가장 중요하다고 생각합니다. 테스트 코드를 작성하는 여러 장점 중 하나가 코드를 수정함에 있어 사이드 이펙트가 발생하지 않는 다는 것을 검증 받을수 있다는 것이라고 생각합니다. 하지만 운영중인 서비스에서 장애가 발생했다면 QA, 개발, 기획 등 대다수의 사람들이 놓쳤던 부분이며, 지속적으로 장애가 발생할 가능성이 있다고 생각합니다. 따라서 장애가 발생한 코드는 반드시 테스트 코드를 작성하여, 동일한 장애가 다시 발생하지 않도록 검증하는 것이 중요하다고 생각합니다.
2. Mock, Any 사용을 최대한 지양하자.
테스트 코드 작성에 회의적인 대부분의 사람들은 시간 들여 테스트 코드를 작성하였지만, 그 보상을 충분히 받지 못하여 회의적인 것이라고 생각합니다. 저는 이러한 보상을 받지 못하는 원인 중 하나가 무분별한 Mock, Any 사용이라고 생각합니다. 너무 지나치게 Mock과 Any를 사용해 버리면 정작 테스트 코드로 검증해야 할 부분을 올바르게 검증하지 못한다는 것입니다.
3. @DisplayName을 자세하게 작성하자
내가 작성한 테스트 코드는 팀의 자산 입니다. 또한 팀과 함께 공유하는 문서 입니다. 테스트 코드 메소드 명으로는 정확하게 테스트 코드가 검증하려고 하는 것을 이해하기 힘들 수 있습니다. 따라서 @DisplayName을 활용하여 자세히 설명 붙여 준다면, 다른 팀원들이 이해하기 더 쉬울 것라고 생각합니다.
- 비추천 : 음료 1개 추가 테스트
- 추천 : 음료를 1개 추가하면 주문 목록에 담긴다. (테스트 행위에 대한 결과까지 문장으로 기술)
4. 경계값 테스트를 위해 FixtureMonkey를 사용해 보자.
테스트 코드를 작성하기 위해 저희는 fixture를 생성합니다. 하지만 저희가 생성한 fixture들은 당연히 테스트 코드를 통과하는 fixture입니다. 정말 장애가 발생하는 fixture는 경계값에서 대부분 발생합니다. 네이버에서 개발한 경계값 생성에 도움을 주는 라이브러리가 있습니다.
FixtureMonkey를 통해 경계값 까지 검증 가능한 테스트 코드 작성을 추천드립니다.
5. BDDMockito를 사용하자. (권장)
BDDMockito에 대해 알아보기 전에 BDD는 TDD에서 파생된 개발 방법론 입니다. 또한 함수 단위의 테스트에 집중하기 보다 시나리오에 기반한 테스트 케이스 자체에 집중하는 테스틑 하는 방법입니다. 많은 개발자 분들이 테스트 코드를 작성할때 무의식 적으로 given-when-then 구조를 많이 활용하실 텐데요.
일반 적인 Mockito를 사용하신다면 given에서 when().thenReturn(); 으로 사용이 됩니다. 약간 어색하지 않나요? given절에서 when을 사용하는게 저는 조금 어색하게 느껴집니다. BDDMockito는 given절에서 given().willReturn(); 형식으로 사용해서 조금더 given-when-then 구조에 맞게끔 네이밍이 되어 있습니다. 동작에는 차이가 없습니다.
- Mockito : when(mailSendClient.send(any()).thenReturn(true);
- BDDMockito : given(mailSendClient.send(any()).willReturn(true);
6. Junit의 Assertion 보다는 AssertJ를 사용하자
JUnit은 기본적인 검증 기능을 제공하지만, AssertJ는 다양하고 가독성이 뛰어난 검증 기능을 제공합니다. AssertJ는 Fluent 형식의 스타일로 작성하기 때문에 가독성이 뛰어나며, 다양한 검증 기능들을 제공합니다.
Junit Assertion
@Test
@DisplayName("Junit의 Assertions을 검증한다.")
void testJunitAssertions() {
int expected = 10;
int actual = 10;
assertEquals(expected, actual); // 두 값이 같은지 확인
assertNotEquals(5, actual); // 값이 다른지 확인
assertTrue(actual > 5); // 조건이 true인지 확인
assertFalse(actual < 5); // 조건이 false인지 확인
}
AssertJ Assertion
@Test
@DisplayName("AssertJ의 기본적인 Assertions를 검증한다.")
void testAssertJAssertions() {
List<String> colors = List.of("Red", "Green", "Blue");
assertThat(colors)
.isNotEmpty()
.hasSize(3)
.contains("Red") // 특정 값 포함 여부
.containsExactly("Red", "Green", "Blue") // 정확한 순서 검증
.containsExactlyInAnyOrder("Blue", "Green", "Red"); // 순서 무관 검증
}
@Test
@DisplayName("AssertJ의 에러 처리와 관련된 Assertions를 검증한다.")
void testExceptionAssertions() {
assertThatThrownBy(() -> {
throw new IllegalArgumentException("Invalid input");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid");
}
테스트 코드는 분명 작업에 시간이 많이 드는 일 인것은 맞습니다. 하지만 테스트 코드를 통해 얻을 수 있는 장점은 테스트 코드에 시간을 투자하기에 충분 하다고 생각합니다. 아직 단위 테스트를 활용하고 있지 않으시다면 조금씩 이라도 작성하여 단위테스트가 주는 장점들을 꼭 누려 보시길 권장드립니다.
'테크톡 딥다이브' 카테고리의 다른 글
| 스프링 Transaction 전파 속성 (0) | 2025.03.31 |
|---|---|
| 네이버의 Fixture Monkey 사용기 (5) | 2025.02.24 |
| 카카오 선물하기 팀의 캐싱 전략 (하이브리드 캐시와 캐시 웜업 자동화) (1) | 2025.01.07 |
| DB Replication (with 우아한 테크 코스's 테크톡) (4) | 2025.01.03 |
| G1GC (with Garbage First GC) (2) | 2025.01.03 |