"unit test는 무의미하다"고 생각한다

White Piano·2023년 8월 31일
0

좌우충돌 사색

목록 보기
6/7

아마 글 제목에 동의하지 못하는 사람이 대부분이라고 생각한다. 이 글은 지침이 아니다. 오히려 나와 같이 배움의 여정 속에서 비슷한 고민을 하는, 하게 될 모든 사람을 더 잘 이해할 수 있는 계기가 되길 바라며 작성한다.

요약

  • unit test는 무의미하다
  • unit test가 아니라 refactoring이 필요한 것이다
  • (integration) test는 필요하다

고민

처음 unit test를 접했을 때, 난 광신도였다. "test"란 기술이 믿을 수 없을 만큼 멋있어 보였다. 하지만 시간이 지나고 머리가 조금 식고 나자 납득할 수 없었다. 그때부터 unit-test는 내게 고민거리가 되었다.

이해를 위해 용어를 먼저 정리한다. function == unit이라 생각해 주길 바란다.

첫 번째 고민, 완벽한 test를 작성할 순 없다

"pass"가 정말로 "pass"라는 증거이길 바랐다. test가 완벽하길 바랐던 거다. 자연스럽게 unit test에 의문을 가지게 됐다.

"미리 준비된 input에, 마찬가지로 미리 준비된 output을 대조할 거라면 무슨 의미가 있지? 이미 계산된, 계산할 수 있는 function이 아닌가?"

정말로 예상치 못한 문제를 발견하는 건 불가능하다. test하는 edge case는 이미 인지하고 있는 case인데 굳이 test해야 할까? 이 단계에서 내렸던 결론은 fuzz testing이었다. 하지만 fuzz testing은 성능 향상을 목적으로 하는 프로그램(느리지만 이미 동작이 검증된 unit으로 output 생성 가능)이 아니라면 적용하기 어렵다.

첫 번째 답변, test의 목적은 보증이 아니다

사실 "pass"가 정말로 pass를 의미하지는 않는다. test 통과는 필요조건이지 충분조건이 아니다. 이 세상에 완벽한 test란 없다. 부족한 test case가 있는지 확인하고 추가하는 작업이 지속해서 이뤄져야 한다.

그리고 테스트를 해당 기능을 만든 사람만 읽는게 아니다. 다른 사람이, 혹은 미래의 자신이 해당 함수를 읽을 때, 테스트 코드를 바탕으로 어떤 예외 사항을 가정하고 작성한 코드인지 알려준다. "이 기능에는 이런 edge case가 있을 것 같은데 고려하고 작성한 코드일까?"와 같은 질문의 답이 test에 담겨 있는 것이다.

두 번째 고민, branch coverage 100%가 필요하다?

모든 code를 test해야 한다 생각했다. 그런데 "모든" code를 test하려다 보니 깊은 회의감이 든다. 이 일이 정녕 필요할까? 모든 일에는 대가가 따른다. test 작성 또한 일이며 시간이 소모된다. 무엇보다 끔찍한 건 code 변경이 test code 갱신으로 이어진다는 것이다. 이건 그냥 일을 두 배로 만들 뿐이다.

TDD? 설계로서의 test? test가 가장 좋은 blueprint라는 말에는 동의하지만, 그것도 필요한 만큼이다. 국어사전은 단어를 설명하면서 초성을 설명하지 않고, 잉크와 여백을 구분하는 방법을 설명하지도 않는다.

두 번째 답변, 필요한 상황에만 작성하면 된다

"test의 의미를 생각해 봐"

그렇다! test를 하는 의미를 생각해야 한다. 분노를 위한 분노가 의미가 없듯, test를 위한 test는 무의미하다. 그렇다면 언제 unit test가 유의미할까? unit이 너무 복잡해서 한 눈에 이해하기 어렵거나 실수할 여지가 생기면 필요하다. 그런 순간이 온다면 test를 작성하면 된다.

세 번째 고민, test가 필요한 만큼 복잡하다면 잘못 작성된 함수다

하나의 함수는, 하나의 책임을 져야 한다. 정말 중요한 법칙이다. 그런데 여기서 "하나의 책임"이란 굉장히 추상적인 말이다. 원자가 원자핵과 전자로 쪼개지듯 어떤 하나의 process는 더 작은 sub-process로 나눠질 수 있다.

한눈에 들어오지 않는 logic? 복잡해서 실수할 수 있는 기능? 그 순간부터 그건 "하나의 책임"이 아니다. 더 작은 sub-unit으로 나눠 설계해야 한다.

가령 우리 프로그램에 등록날짜에 따라 회원의 기수를 구분하는 함수가 있다고 하자.
각 기수별 활동기간이 다르기 때문에 이 함수는 상당히 복잡해질 수 있다.
그래서 나는 unit-test를 작성했다.

위 예를 어떻게 처리할 수 있을까? 더 작은 sub-unit으로 나눌 수 있지 않을까?

우선 data와 logic의 분리가 필요하다. 각 기수별 활동 시간이 다르다면 기수별 활동 시작일과 종료일을 상수로 빼내고 logic에서 iterating 하면 그만이다. 아니면 기수별로 해당 기수가 맞는지 test하는 sub-function을 정의하고 내부적으로 호출하면 그만이다. 물론 각각의 sub-function은 unit test가 필요할 정도로 복잡하지 않을 것이다. (복잡하다면 다시 쪼개면 그만이다)

하지만 위 기능은 여전히 test가 필요하다. 충분히 실수할 수 있는 복잡도를 가진 기능이니까. 하지만 그건 unit-test가 아닌 integration-test가 되어야 한다.

위 함수를 unit-test하려면 각종 library를 통해 mock를 만들어서 의존 관계를 없애야 한다. 그런데 그렇게 작고 단순하고 하나의 책임만을 가지는 여러 unit을 mock 해서 하는 test가 정녕 의미가 있는가? test해야 하는건 "unit"이 아닌 "functionality"다.

세 번째 답변, unit test란 뭘까?

혹자는 unit-test와 integration-test를 그렇게 엄격하게 구분하지 않는다고 한다. 흔히 얘기하는 server-side의 3-layered-architecture를 예로 들자면, 개개의 service를 하나의 unit으로 취급하기 때문에, repository와 다른 service만 mock 한다면 unit-test라고 볼 수 있다는 것이다. unit-test라고 하는게 이러한 맥락에서 사용된다고 한다면 필요성에 동의한다.

하지만 난 그 맥락에 동의할 수 없다. unit-test의 의미는, 외부와 완전히 단절되어 문제의 발생 원인과 여지가 그 자신에게 한정되는 데에 있다. 의미론적인 unit은 말이 안 된다. test 단계에서 unit은 무엇보다도 설계적이어야 한다.

function(A)가 호출하는 다른 function(B)가 해당 function(A)에게만 연결되어 있다는 보장이 있는가? 당장 그러하더라도 앞으로도 그럴 거라고 보장할 수 있는가?

0개의 댓글