[CleanCode] -9. 단위 테스트

Young Min Sim ·2021년 4월 21일
0

CleanCode

목록 보기
9/16
post-custom-banner

1. TDD 법칙 세 가지

  • 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  • 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

즉, 테스트 코드 먼저 작성 후 최대한 빠르게 테스트를 통과할 정도로만 구현 코드 작성하고 리팩토링

하지만 이렇게 나오는 실제 코드와 맞먹을 정도의 방대한 테스트 코드는 관리 문제를 유발한다.


2. 깨끗한 테스트 코드 유지하기

지저분한 테스트 코드를 만드는 것은 테스트 코드를 만들지 않는 것만 못하다.
테스트 코드는 실제 코드 못지 않게 깨끗하게 짜야 한다.

  • 테스트는 유연성, 유지보수성, 재사용성을 제공한다.
    • 테스트 케이스가 있으면 변경이 용이하다.
    • 테스트 케이스가 없으면 모든 변경이 잠정적인 버그다.
  • 그렇기 때문에 테스트 케이스를 깨끗하게 유지해야 한다.
    그렇지 않으면 코드를 변경하는 능력, 구조를 개선하는 능력이 떨어지고 실제 코드도 지저분해진다.
    결국 테스트 코드를 잃어버리고 실제 코드도 망가진다.

3. 깨끗한 테스트 코드

깨끗한 테스트 코드를 만들려면 ? 가독성이 무엇보다 중요하다.
실제 코드보다 더 중요하다고 할 정도로 중요하다.

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>"
  );
}

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
  WikiPage page = makePage("PageOne");
  makePages("PageOne.ChildOne", "PageTwo");

  addLinkTo(page, "PageTwo", "SymPage");

  submitRequest("root", "type:pages");

  assertResponseIsXML();
  assertResponseContains(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
  );
  assertResponseDoesNotContain("SymPage");
}

public void testGetDataAsXml() throws Exception {
  makePageWithContent("TestPageOne", "test page");

  submitRequest("TestPageOne", "type:data");

  assertResponseIsXML();
  assertResponseContains("test page", "<Test");
}

BUILD-OPERATE-CHECK 패턴

첫 부분은 테스트 자료를 만든다.
두 번째 부분은 테스트 자료를 조작한다
세 번째 부분은 조작한 결과가 올바른지 확인한다.

테스트 코드잡다하고 세세한 코드가 없어야 하고 진짜 필요한 자료 유형과 함수만 사용해야 한다.
(== 가독성이 좋아야 한다)


이중 표준

  • 테스트 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.
  • 아래가 그 예로, 세세한 코드가 많아 이를 좀 더 줄일 수 있다.
  • 개선 전
@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());
}
  • 개선 후
    실제 코드였다면 권장될 방식은 아니지만, 테스트코드에서는 heater, blower, cooler... 등의 순서이고
    대문자는 켜짐(On), 소문자는 꺼짐(Off) 임을 안다면 이해할 수 있다.
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
  wayTooCold();
  assertEquals("HBchL", hw.getState()); 
}

이를 응용해서 아래와 같이 테스트 코드를 추가할 수 있다.

@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
  tooHot();
  assertEquals("hBChl", hw.getState()); 
}

@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
  tooCold();
  assertEquals("HBchl", hw.getState()); 
}

@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
  wayTooHot();
  assertEquals("hBCHl", hw.getState()); 
}

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
  wayTooCold();
  assertEquals("HBchL", hw.getState()); 
}

이럴 때 주석이 필요하지 않나 싶습니다.


4. 테스트 당 개념 하나

아래의 예제는 각 절에 assert 문이 여럿이라는 사실이 문제가 아니다.
하나의 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제다.
독자적인 테스트로 쪼개야 마땅하다.

public void testAddMonths() {
  SerialDate d1 = SerialDate.createInstance(31, 5, 2004);

  SerialDate d2 = SerialDate.addMonths(1, d1); 
  assertEquals(30, d2.getDayOfMonth()); 
  assertEquals(6, d2.getMonth()); 
  assertEquals(2004, d2.getYYYY());

  SerialDate d3 = SerialDate.addMonths(2, d1); 
  assertEquals(31, d3.getDayOfMonth()); 
  assertEquals(7, d3.getMonth()); 
  assertEquals(2004, d3.getYYYY());

  SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); 
  assertEquals(30, d4.getDayOfMonth());
  assertEquals(7, d4.getMonth());
  assertEquals(2004, d4.getYYYY());
}

작명이 좋진 않지만..
testAddMonthOfEndDay30, testAddMonthOfEndDay31 등이 될 수 있을 것 같네요.

그러므로 가장 좋은 규칙은 아래와 같다.

  • 테스트 함수 하나에는 개념 하나
  • 개념 하나 당 assert 문을 최소로 줄여라

5. F.I.R.S.T

깨끗한 테스트는 다음 다섯 가지 규칙을 따른다.

  • 빠르게(Fast): 테스트가 느리면 자주 돌릴 엄두를 못 내고 자주 돌리지 못하면 초반에 문제를 찾아내 고치지 못한다.
  • 독립적으로(Independent): 각 테스트는 서로 의존하면 안 된다. 테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워진다.
  • 반복가능하게(Repeatable): 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • 자가검증하는(Self-Validating): 테스트는 부울 값으로 결과를 내야 한다. (성공 or 실패)
  • 적시에(Timely): 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

결론

깨끗한 테스트 코드라는 주제는 책 한권을 할애해도 모자랄 주제인 만큼 실제 코드만큼이나 중요하다.
테스트 코드가 방치되어 망가지면 실제 코드도 망가지므로 테스트 코드를 깨끗하게 유지해야 한다.

post-custom-banner

0개의 댓글