클린 코드 #9 단위 테스트
과거에 비하면 테스트 분야가 엄청난 성장을 이뤘고, 단위 테스트를 자동화하는 프로그래머가 많아지고 있다.
하지만, 많은 프로그래머들이 제대로 된 테스트 케이스를 작성해야 한다는 미묘하면서 중요한 사실을 놓치고 있다.
TDD 법칙 세 가지
TDD는 실제 코드를 짜기 전에 단위 테스트부터 짜라고 요구한다. 하지만 이 규칙 외에도 많은 규칙들이 있다.
- 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 컴파일은 실패하지 않으면서 실행 시 실패하는 정도로만 단위 테스트를 작성한다.
- 현재 실패하는 테스트를 통과할 정도로만 테스트를 작성한다.
이렇게 일하면, 매일 수 십개의 테스트 케이스가 나온다. (1달이면 수 백개…) 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
깨끗한 테스트 코드 유지하기
실제 코드와 테스트 코드에 동일한 품질 기준을 적용하지 않는 팀이 있을 수도 있다. 이 팀은, “지저분해도 빨리”가 목표인 것이다.
하지만 이 팀은 지저분한 테스트 코드를 내놓을 바에는 테스트를 안 하는 것보다 못하다는 사실을 깨닫지 못했다.
문제는 실제 코드가 진화하면 테스트 코드도 변해야 한다는 데에 있다.
- 테스트 코드가 지저분할수록 변경하기 어려워진다.
- 테스트 코드가 복잡할 수록 실제 코드 작성 시간보다 테스트 케이스를 추가하는데 시간이 더 많이 소요된다.
- 실제 코드를 변경해서 기존 테스트 케이스가 실패하기 시작하면 기존의 지저분한 코드 때문에 테스트 케이스를 통과하기 더 어려워진다.
결국, 테스트 코드는 부담이 되어버린다.
새 버전 출시할 때 테스트 케이스를 유지하고 보수하는 비용도 늘어난다. 개발자는 항상 이에 불만이다.
하지만 테스트 슈트가 없으면 개발자가 작성한 코드가 제대로 동작하는지 확인할 방법이 없다. 결국 결함율이 높아진다. 결함율이 높아지면 개발자는 변경을 주저한다. 득보다 해가 크다고 판단하여 코드를 정리하지 않게 되고 코드가 망가진다.
지저분한 테스트 코드에 쏟은 노력은 허사였다. 하지만 이 근본적인 원인은 테스트 코드를 막 짜도 좋다고 허용한 결정이었다.
저자가 전하고 싶은 교훈은 다음과 같다.
“테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 이류 시민이 아니다.”
테스트는 유연성, 유지보수성, 재사용성을 제공한다.
테스트 코드를 깨끗하게 유지하지 않으면 결국 잃어버리게 된다. 테스트 코드가 사라지면 실제 코드도 유연해지지 않는다.
코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 단위 테스트다. 테스트 케이스가 있으면 변경이 두렵지 않기 때문이다.
(테스트 케이스가 없으면 모든 것이 잠정적인 버그이다. 개발자는 변경을 두려워하게 된다.)
테스트 코드가 있고 커버리지가 높을 수록 공포는 줄어든다. 아키텍처 설계도 안심하고 개선할 수 있다.
실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존해준다.
테스트 코드가 지저분하면 코드를 변경하는 능력이 떨어지고 구조 개선 능력도 떨어진다. 결국 테스트 코드를 잃어버리고 실제 코드도 망가진다.
깨끗한 테스트 코드
깨끗한 테스트 코드를 만들기 위해서는 다음 세 가지가 필요하다
- 가독성
- 가독성
- 가독성
실제 코드에서보다 테스트 코드에서 가독성은 더 중요하다. 명료하고 단순하고 풍부한 표현력이 필요하다. 최소의 표현으로 많은 것을 나타내야 한다.
Before
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
request.addXXX();
request.setXXX();
assertEquals(...);
// ...
After
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertEquals(...);
세세하고 잡다한 코드를 거의 다 없앴다. (변하는 것과 변하지 않는 것을 분리되었다.)
이러한 테스트에서는 BUILD-OPERATE-CHECK 패턴이 적합하다.
- BUILD : 테스트 자료를 만든다.
- OPERATE : 테스트 자료를 조작한다.
- CHECK : 조작의 결과를 확인한다.
이제 코드를 읽기 아주 수월해진다.
도메인에 특화된 테스트 언어
위에서 After를 봐보면, 도메인에 특화된 언어(DSL)로 테스트 코드를 구현하는 기법을 보여준다.
시스템 조작 API를 직접 사용하는게 아니라, API 위에 함수와 유틸리티를 구현하고 그 함수와 유틸리티를 사용하므로, 테스트 코드를 짜기도 읽기도 쉬워진다. 나중에 테스트를 읽어볼 독자를 도와주는 일종의 테스트 언어이다.
테스트 API는 처음부터 설계된 API가 아니다. 잡다한 세부사항을 리팩토링하면서 진화된 API다. 테스트 코드도 간결하고 표현력이 풍부한 코드로 리팩토링하자.
이중 표준
테스트 API에 적용하는 표준은 실제 코드에 적용되는 표준과 다르다. 표현이 풍부하고, 간결하고… 그렇지만 실제 코드만큼 효율적일 필요는 없다. 테스트 환경에서 돌아가는 코드이기 때문이다. 실제 환경과 테스트 환경은 요구사항이 다르다.
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.coolerState());
코드에 세세한 사항이 많다. hw? tic?
하지만 세세하게 설명하지 않더라도 온도가 급격하게 떨어지면 히터, …가 가동되는지 확인하는 코드라는 사실이 드러난다.
아쉬운점이 있다면 코드에서 점검하는 상태 이름과 상태 값을 확인하느라 눈길이 이리저리 흩어진다는 점이다. 테스트 코드를 읽기 조금 어렵다.
이를 리팩토링해서 가독성을 높인다.
wayTooCold();
assertEquals("HBchL", hw.getState());
- 대문자는 켜짐, 소문자는 꺼짐이다.
- 물론 “그릇된 정보는 피하라”라는 규칙에는 위반되지만 이 테스트에서는 적절하다.
또한 hw.getState()
에서 문자열로 변환할 때 String 더하기를 하든 StringBuilder를 쓰든 상관이 없다. 테스트 환경이기 때문!
이것이 이중 표준의 본질이다. 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제 없다.
오해하지 말자. 코드의 깨끗함과는 철저히 무관하다. 코드는 테스트 환경에서도 깨끗해야 한다.
테스트 당 assert 하나
JUnit 테스트 시, 함수마다 assert를 하나만 사용해야 한다는 학파가 존재한다. 매우 가혹하지만, 확실한 장점이 있다.
assert문이 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.
하지만 테스트를 분리하면 중복되는 코드가 많아진다. 이 때, 템플릿 메서드 패턴을 사용하면 중복을 제거할 수 있다.
given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.
아니면 독자적인 테스트 클래스를 만들어 @Before
에 given/when을 두고, @Test
부분에 then을 넣어도 된다.
하지만 저자는 아무리 봐도 배보다 배꼽이 크다고 생각한다. 저자는 assert문을 여럿 사용하는 것이 좋다고 생각한다.
단일 assert문은 훌륭한 지침이다. 하지만 함수 하나에 여러 assert문을 넣기도 한다. assert문의 개수를 막 1개로 맞추려는 것 보단 최대한 줄이는 게 좋다.
테스트 당 개념 하나
테스트 당 assert 하나라는 말보단, “테스트 함수마다 한 개념만 테스트하라” 라는 규칙이 더 낫다.
이것 저것 연속으로 테스트 하는 긴 함수는 피하자. 테스트를 쪼개는 게 좋다.
테스트를 쪼개도 보면 테스트 코드 속에 감춰진 일반적인 규칙이 보이고, 빠진 테스트 케이스가 보이게 된다.
F.I.R.S.T
깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.
F. Fast(빠르게)
- 테스트는 빨라야 한다. 빨리 돌아야 자주 돌릴 수 있고 코드를 마음 껏 정리할 수 있다.
I. Independent(독립적으로)
- 각 테스트는 서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다. 순서도 영향을 주면 안 된다.
R. Repeatable(반복 가능하게)
- 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA, 모바일 환경, …
S. Self-Validating(자가검증하는)
- 테스트는 부울 값으로 결과를 내야 한다. 성공/실패 둘 중 하나다. 테스트 통과 여부를 알려고 로그 파일을 읽게 만들면 안 된다.
T. Timely(적시에)
- 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 뒤늦게 실제 코드가 테스트하기 어렵다는 점을 발견하게 된다. 극단적으로는 테스트 불가능하게 실제 코드를 설계할 수도 있다.
결론
깨끗한 테스트 코드에 대해서는 책으로 다룰만큼 분량이 많다.
여기서 저자가 말하고 싶은 핵심은, 테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다는 점이다.
테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화한다. 따라서
- 테스트 코드는 지속적으로 깨끗하게 관리하자.
- 표현력을 높이고 간결하게 정리하자
- 테스트 API를 구현해 DSL를 만들자.
테스트 코드가 방치되면 망가져서 실제 코드도 망가진다. 테스트 코드는 깨끗하게 유지하자.
'Software Engineering > Clean Code' 카테고리의 다른 글
[클린 코드] #10 클래스 (1) | 2025.09.03 |
---|---|
[클린 코드] #8 경계 (0) | 2025.09.03 |
[클린 코드] #7 오류 처리 (0) | 2025.09.03 |
[클린 코드] #6 객체와 자료구조 (0) | 2025.09.03 |
[클린 코드] #5 형식 맞추기 (0) | 2025.09.03 |