테스트는 빠르게 작성되어야 한다. 하지만, '잘' 작성되어야 한다.
TDD 법칙 세 가지
TDD는 어떤 법칙인가? 많은 사람들이 '코드를 짜기 전 단위 테스트부터 짜는 것'을 떠올릴 것이다.
하지만 이 규칙은 빙산의 일각에 불과하다. 다음 세 가지 법칙을 살펴보자.
- 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
이렇게 세 가지 규칙을 따르면서 테스트 코드를 작성하면, 매일 수십 개, 매달 수백 개에 달하는 테스트 케이스가 나온다.
이렇게 일하면 사실상 실제 코드를 전부 테스트하는 테스트 케이스가 나온다.
하지만 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
깨끗한 테스트 코드 유지하기
테스트 코드는 돌아가기만 하면 될까?
지저분하지만 돌아가기는 하는 테스트 코드를 가지고 있다면, 이를 자동화된 단위 테스트 슈트로 바꾸기란 쉽지 않다.
하지만 지저분한 테스트 코드를 내놓는 것은 테스트를 안하는 것보다 못하다.
문제는 실제 코드가 진화하면 테스트 코드도 변해야 한다는 데 있다.
그런데 테스트 코드가 지저분하면 테스트 코드를 변경하기 어려워진다.
새 버전을 출시할 때마다 테스트 케이스를 유지하고 보수하는 비용도 늘어난다.
결국 테스트 슈트를 폐기하지 않으면 안되는 상황에 처한다.
하지만 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없다.
그래서 시스템의 결함율이 높아지기 시작한다.
이처럼 테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 사고와 설계와 주의가 필요하다.
실제 코드 못지 않게 깔끔하게 짜야한다.
- 테스트는 유연성, 유지보수성, 재사용성을 제공한다
테스트 코드를 깨끗하게 유지하지 않으면 결국은 잃어버린다.
그리고 테스트 케이스가 없으면 실제 코드를 유연하게 만드는 버팀목도 사라진다.
코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다.
이유는 단순하다. 테스트가 있으면 변경에 두렵지 않기 때문이다.
깨끗한 테스트 코드
깨끗한 테스트 코드를 만들려면? 세 가지가 필요하다. 가독성, 가독성, 가독성.
가독성은 실제 코드보다 테스트 코드에서 더 중요하다. 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.
@Test
void 포테이토_피자_만들기_테스트() {
반죽한다();
도우를_만든다();
도우에_소스를_뿌린다();
도우에_포테이토를_올린다();
오븐에_넣는다();
기다린다();
오븐에서 뺀다();
assertThat(오븐에서_뺀_피자).isEqualTo(포테이토피자);
}
위 테스트 코드의 문제점은 무엇일까? 잡음이 너무 많다. 읽는 사람을 고려하지 않는다.
독자들은 온갖 잡다하고 무관한 코드를 이해하고 나서야 테스트 케이스를 이해한다.
@Test
void 포테이토_피자_만들기_테스트() {
// given
포테이토_피자_도우를_만든다();
// when
오븐에_굽는다();
// then
assertThat(오븐에서_뺀_피자).isEqualTo(포테이토피자);
}
위 예제에서는 잡다하고 세세한 코드를 거의 다 없앴다. 또한 위 코드는 명확하게 세 부분으로 나눠진다.
1. 테스트 자료를 만드는 과정(given), 2. 테스트 자료를 조작하는 과정(when), 3. 조작한 결과가 올바른지 확인하는 과정(then).
코드를 읽는 사람은 '피자를 만드는 과정'이 아닌 '테스트 케이스'에 집중할 수 있게 된다.
테스트가 수행하는 일이 무엇인지 바로 알 수 있게 된다.
- 도메인에 특화된 테스트 언어
위 예시처럼 도메인에 특화된 언어(DSL)로 테스트 코드를 구현할 수 있다.
흔히 쓰는 시스템 조작 API를 사용하는 대신, API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용하므로
테스트 코드를 짜기도 읽기도 쉬워진다.
즉, 테스트를 구현하는 당사자와 나중에 테스트를 읽어볼 독자를 도와주는 테스트 언어이다.
이러한 API는 처음부터 설계된 API가 아니다. 리팩토링을 통해 얻어진 API다. 숙련된 개발자라면 자기 코드를 조금 더 간결하고
표현력이 풍부한 코드로 리팩터링해야 마땅하다.
- 이중 표준
테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.
단순하고, 간결하고, 표현력이 풍부해야 하지만, 실제 코드만큼 효율적인 필요는 없다.
실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문이다.
@Test
public void turnOnLoTempAlarmAtThreashold() {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.lotempAlarm());
}
위 코드는세세한 사항이 아주 많다. 상태 이름과 상태 값을 확인하느라 눈길이 이리저리 흩어진다.
테스트 코드를 읽기가 어렵다. 다음과 같은 코드는 어떨까?
@Test
public void turnOnLoTempAlarmAtThreshold() {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
tic 함수는 wayTooCold라는 함수로 숨겼다.
"HBchL" 이라는 건 무엇을 의미하는 걸까? {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm} 순서이고,
대문자는 '켜짐', 소문자는 '꺼짐'을 의미한다.
비록 위 코드는 '그릇된 정보를 제공하지 마라' 라는 규칙을 위반하기는 하지만, 테스트 코드에는 특화되었다. 이해하기가 쉽다.
이것이 이중 표준의 본질이다. 실제 환경에서는 절대로 안되지만, 테스트 환경에서는 전혀 문제없는 방식이 있다.
테스트 당 assert 하나
몇몇 학파는 테스트 코드를 짤 때는 함수 하나마다 assert문 하나를 작성해야 한다고 주장한다.
하지만 이러한 규칙을 무작정 적용하는 것은 좋지 못하다.
의미적으로 군집되어 있는 assert 문 여러개를 수행할 때, 이것을 단순히 '함수 하나 당 assert 문 한 개'의 규칙 아래
여러 함수로 분리한다면 중복되는 코드가 많아진다.
TEMPLATE METHOD 패턴이나 상속을 이용하면 중복을 제거할 수는 있다. 하지만 이는 배보다 배꼽이 더 크다.
assert문을 여러 개 사용하는 편이 더 좋다.
'단일 assert 문' 이 훌륭한 지침이기는 하지만, 맹목적으로 따를 필요는 없다. 단지 assert문 개수는 최대한 줄여야 한다는 것에는 동의한다.
하지만 '테스트 함수마다 한 개념만 테스트하라' 라는 규칙이 더 좋다.
F.I.R.S.T
깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.
1. 빠르게(Fast): 테스트는 빨라야 한다. 빨리 돌아야 한다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다. 자주 돌리지 못하면 초반에 문제를 찾아내 고치지 못한다. 코드를 마음껏 정리하지도 못한다. 결국 코드 품질이 망가진다.
2. 독립적으로(Independent): 각 테스트는 서로 의존하면 안된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다. 각 테스트는 독립적으로, 그리고 어떤 순서로 실행해도 괜찮아야 한다. 테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다.
3. 반복가능하게(Repeatable): 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA 환경, 네트워크가 작동하지 않는 환경 등, 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
4. 자가검증하는(Self-validating): 테스트는 부울 값으로 결과를 내야 한다. '성공 아니면 실패'. 통과 여부를 알려고 텍스트 파일을 읽어야 한다거나, 로그 파일을 읽게 만들면 안된다. 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적으로 바뀐다.
5. 적시에(Timely): 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드로 구현하기 직전에 구현한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다. 어떤 실제 코드는 테스트하기 너무 어렵다고 판명날지 모른다. 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.
'클린 코드' 카테고리의 다른 글
[클린 코드] 10장: 클래스 (0) | 2022.12.21 |
---|---|
[클린 코드] 8장: 경계 (0) | 2022.12.03 |
[클린 코드] 7장: 오류 처리 (0) | 2022.12.02 |
[클린 코드] 6장: 객체와 자료 구조 (0) | 2022.11.29 |
[클린 코드] 5장: 형식 맞추기 (0) | 2022.11.29 |