TDD 법칙 세 가지
- 첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
- 세 가지 규칙을 따르면 테스트 코드와 실제 코드가 함께 나올뿐더러 테스트 코드가 실제 코드보다 불과 몇 초 전에 나온다.
- 이렇게 일하면 실제 코드를 사실상 전부 테스트하는 테스트 케이스가 나온다.
- 하지만 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
깨끗한 테스트 코드 유지하기
- 실제 코드가 진화하면 테스트 코드도 변해야 한다. 그런데 테스트 코드가 지저분할수록 변경하기 어려워진다.
- 테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸리기 십상이다.
- 하지만 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없다.
- 테스트 코드는 실제 코드 못지 않게 중요하다. 실제 코드 못지 않게 깨끗하게 짜야 한다.
- 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 테스트 케이스가 있으면 변경이 두렵지 않다.
- 테스트 코드가 없다면 모든 변경이 잠정적인 버그다.
- 그러므로 실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다.
깨끗한 테스트 코드
- 깨끗한 테스트 코드를 만들려면 가독성, 가독성, 가독성 세 가지가 필요하다.
- 어쩌면 가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다.
- 테스트 코드에서 가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다.
- 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.
- BUILD-OPERATE-CHECK 패턴은 각 세트스를 명확히 세 부분으로 나눈다.
첫 부분은 테스트 자료를 만든다. 두 번째 부분은 테스트 자료를 조작하며, 세 번째 부분은 조작한 결과가 올바른지 확인한다.
- 잡다하고 세세한 코드는 거의다 없애고 본론에 돌입해 진짜 필요한 자료 유형과 함수만 사용한다.
그러므로 코드를 읽는 사람은 온갖 잡다하고 세세한 코드에 주눅들고 헷갈릴 필요 없이 코드가 수행하는 기능을 재빨리 이해한다.
- 도메인에 특화된 언어로 테스트 코드를 구현할 경우,
API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.
// good
// BUILD-OPERATE-CHECK 패턴 + DSL
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}
- 테스트 케이스는 실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문에
단순하고, 간결하고, 표현력이 풍부해야 하지만, 실제 코드만큼 효율적인 필요는 없다.
- 실제 애플리케이션과 달리 테스트 환경은 자원이 제한적인 가능성이 낮으므로 크게 무리가 아니라면 보기 흉한 코드는 피한다.
// bad
// 온도가 급격히 떨어디면 경보, 온풍기, 송풍기가 모두 가동되는지 확인하는 코드
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
// good
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
wayTooCold(); // tic 함수가 숨어들게 됨
assertEquals("HBchL", hw.getState());
}
public String getState() {
String state = "";
state += heater ? "H" : "h"; // 대문자는 켜짐, 소문자는 꺼짐을 뜻함
state += blower ? "B" : "b";
state += cooler ? "C" : "c";
state += hiTempAlarm ? "H" : "h";
state += loTempAlarm ? "L" : "l";
}
테스트 당 assert 하나
- JUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있다.
assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해가 쉽고 빠르다.
- assert 문이 두 개라면 테스트를 두 개로 쪼개 각자가 assert를 수행하면 된다.
- given-when-then이라는 관계를 사용하면 테스트 코드를 읽기 쉬워진다.
// good
// given-when-then
public void testGetPageHierarchyAsXml() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}
- 하지만 테스트를 분리하면 중복되는 코드가 많아지므로 TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다.
given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.
아니면 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 부분에 then을 넣는다.
- 하지만 모두 배보다 배꼽이 더 크므로 이것저것 감안해 보면 결국 assert 문을 여럿 사용하는 편이 좋다.
- 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피하고 테스트 함수마다 한 개념만 테스트하도록 쪼개야 마땅하다.
- 가장 좋은 개념은 "개념 당 assert 문 수를 최소로 줄여라"와 "테스트 함수 하나는 개념 하나만 테스트하라"이다.
F.I.R.S.T.
- 깨끗한 테스트는 다섯 가지 규칙을 따른다.
빠르게(Fast), 독립적으로(Independent), 반복가능하게(Repeatable), 자가검증하는(Selft-Validating), 적시에(Timely)
- 테스트는 빨라야 한다. 테스트가 느리면 자주 돌릴 엄두를 못 내어 초반에 문제를 찾아내고치지 못하며 정리하지도 못한다.
- 각 테스트는 서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다.
각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
- 테스트는 어떤 환경에서도 반복 가능해야 한다. 테스트가 돌아가지 않는 환경이 하나라도 있으면 안 된다.
- 테스트는 부울 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안 된다.
- 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.