모르지 않다는 것은 아는것과 다르다.

Books

클린코드 9장 (단위 테스트)

채마스 2022. 4. 30. 11:16

단위 테스트

  • 단위 테스트는 모듈(클래스) 단위로 정상적으로 작동하는지 모든 메소드에 대해 정상적으로 작동하는지 테스트하는것이다.
  • 하지만 많은 프로그래머들이 제대로 된 테스트 케이스를 작성해야한다는 중요한 사실을 놓쳐버린다.

 

TDD 법칙 세 가지

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

 

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

  • 테스트 코드를 작성한다고 작성하지 않을때 보다 무조건 좋은것은 아니다.
  • 테스트 코드를 깨끗하게 짜는 것이 중요하다.
  • 깨끗하게 코드를 짜지 않는다면 새 버전을 출시 할 때마다 팀이 테스트 케이스를 유지하고 보수하는 비용이 늘어난다.
  • 결함율이 높아지고, 이로인해 변경하면 득보다 해가 더 크다고 생각해 코드를 정리하지 않게 될 수도있다.
  • 또한 테스트에 쏟은 시간또한 허사로 돌아간다.
  • 그렇기 때문에, 테스트 코드는 실제 코드 못지 않게 중요하다.

 

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

  • 테스트 코드를 깨끗하게 유지하지 않으면 결국 폐기할 수 밖에 없는 코드가 된다.
  • 그렇다고 테스트 코드가 없다면 실제 코드를 유연하게 만드는 버팀목이 사라지고, 그로 인해서 개발자는 버그가 두려워 변경을 주저한다.
  • 하지만 테스트 케이스가 있다면 공포는 사라지게 된다.
  • 실제 코드를 점검하는 자동화된 단위 테스트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다.
  • 테스트 케이스가 있으면 변경이 쉬워지기 때문이다.
  • 따라서 테스트 코드가 지저분해지면 코드를 변경하는 능력이 떨어지며 코드 구조를 개선하는 능력도 떨어진다.
  • 테스트 코드가 지저분할수록 실제 코드도 지저분해진다.
  • 결국 테스트 코드를 잃어버리고 실제 코드도 망가진다.

 

깨끗한 테스트 코드

  • 테스트 코드를 작성함에 있어서 가장 중요한 부분은 가독성이다.
  • 그러기 위해서는 명료성, 단순성, 풍부한 표현력이 필요하다.
  • 그래야만 최소의 표현으로 많은 것을 나태낼 수 있다.
  • 아래의 코드를 보자
public void testGetPageHieratchyAsXml() throws Exception {
    crawler.addPage(root, PathParser.parse("PageOne"));
    crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
    crawler.addPage(root, PathParser.parse("PageTwo"));

    request.setResource("root");
    request.addInput("type", "pages");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);

    String xml = response.getContext();

    assertEquals("test/xml", response.getContextType());
    assertSubString("<name>PageOne</name>", xml);
    assertSubString("<name>PageTwo</name>", xml);
    assertSubString("<name>ChildOne</name>", xml);
}


public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() throws Exception {
    WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
    crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
    PageData data = pageOne.getData();
    WikiPageProperties properties = data.getProperties();
    WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
    symLinks.set("SymPage", "PageTwo");
    pageOne.commit(data);

    request.setResource("root");
    request.addInput("type", "pages");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
    String xml = response.getContent();

    assertEquals("text/xml", response.getContentType());
    assertSubString("<name>PageOne</name>", xml);
    assertSubString("<name>PageTwo</name>", xml);
    assertSubString("<name>ChildOne</name>", xml);
    assertNotSubString("SymPage", xml);
}

public void testGetDataAsHtml() throws Exception {
    crawler.addPage(root, PathParser.parse("TestPageOne", "test page");

    request.setREsource("TestPageOne");
    request.addInput("type", "data");
    Responder responder = new SerializedPageResponder();
    SimpleResponse response = (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
    String xml = response.getContext();

    assertEquals("text/xml", response.getContextType());
    assertSubString("test page", xml);
    assertSubString("<Test", xml);
}
  • 위의 코드는 개선할 여지가 많다.
  • 먼저 addPage, assertSubString 이라는 코드가 중복적으로 등장한다.
  • 그로인해 자질구레한 사항이 많아 코드의 표현력이 떨어진다.
  • 또한 문자열을 pagePath 인스턴스로 변환하는 PathParser 또한 의도를 흐린다.
  • 그 뿐만아니라 responder 객체를 생성하는 코드와 response를 수집해 변환하는 코드 역시 잡음에 불과하다.
  • 위의 코드는 아래와 같이 리팩토링 될 수 있다.
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 패턴이 위와 같은 테스트 구조에 적합하다.
  • 각 테스트를 3가지 부분으로 나눈다.
    • 테스트 자료를 만든다.
    • 테스트 자료를 조작한다.
    • 조작한 결과가 올바른지 확인한다.
  • 이렇게 리팩토링된 코드는 도메인에 특화된 언어(DSL)라고 할 수 있다.
    • 시스템 조작 API를 사용하는 대신 API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용한다.
    • 테스트 코드를 짜기도 읽기도 쉬워진다.
    • 현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 된다.
    • DSL은 잡다하고 세세한 사항으로 번벅된 코드를 계속 리팩토링해서 진화한 API이며, 나중에 테스트를 읽어볼 독자를 도와주는 언어이다.

 

이중 표준

  • 테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.
  • 단순하고, 간결하고 표현력이 풍부해야 하지만, 실제 코드만큼 효율적일 필요는 없다.
  • 실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문이다.
  • 아래의 코드를 보자
@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());
}
  • 딱봐도 세세한 사항이 아주 많다.
  • 각각의 상태를 보고 그 상태가 True 인지 False 인지 봐야한다.
  • 결국 읽기가 어렵고 따분하다.
  • 아래와 같이 리팩토링하면 어떨까
@Test
public void turnOnLoTempAlarmThreshold() throws Exception {
    wayTooCold();
    assertEquals("HBchL", hw.getState());
}
  • HBchL: (heater, blower, coolear, hi-temp-alarm, lo-temp-alarm) 이며 대소문자로 꺼진것과 켜진 것을 구분해서 한눈에 알아 보기 쉽도록 하였다.
  • 물론 이렇게 코드를 축약해서 표현하는 것을 지양해야하지만, 특수한 경우 다음과 같이 오히려 좋은 경우가 있다.
  • 아래와 같이 상태에 다양한 테스트를 한다고 했을때 한눈에 코드가 읽히기 때문이다.
@Test
public void turnOnCollerAndBlowerIfTooHot() throws Exception {
    tooHot();
    assertEquals("hBChl", hw.getState());
}

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

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

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

 

테스트 당 assert 하나

  • 테스트 당 assert 하나 보다는 테스트 함수마다 한 개념만 테스트하라라는 규칙이 더 좋다.
  • 또한 개념당 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());
}
  • 위의 코드는 3가지 개념을 테스트하는 코드이다.
    • 개념1: (6월처럼) 30일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안된다.
    • 개념2: 두 달을 더하면 그리고 두 번째 달이 31일로 끝나면 날짜는 31일이 되어야 한다.
    • 개념3: 31일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안된다.
  • 개념 3개를 각각 분리해서 1개의 개념씩 테스트 하는 것이 좋다.

 

FIRST (깨끗한 테스트를 짜기위한 5가지 규칙)

  • Fast
    • 테스트는 빨리 돌아야 한다.
    • 테스트가 느리면 자주 돌림 엄두가 나지 않고, 초반에 문제를 찾아 고치지 못한다.
    • 결과적으로 코드 품질이 망가지게 된다.
  • Independent
    • 각 테스트는 서로 의존하면 안된다.
    • 각 테스트는 독립적으로, 어떤 순서로 실행해도 괜찮아야 한다.
  • Repeatable
    • 테스트는 어떤 환경에서도 반복 가능해야 한다.
    • 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패할 이유를 둘러댈 변명이 생긴다.
  • Self-Validating
    • 테스트는 성공 아니면 실패로 결과를 내야 한다.
    • 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안된다.
    • 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다.
  • Timely
    • 테스트는 적시에 작성해야 한다.
    • 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
    • 실제 코드를 구현한 다음에 테스트 코드를 만들면 테스트가 어렵도록 실제 코드를 설계할 수도 있다.




REFERENCES

  • 클린코드 9장

'Books' 카테고리의 다른 글

이펙티브 자바 2장 (객체 생성과 파괴)  (0) 2022.05.30
클린코드 10장  (0) 2022.04.30
클린코드 8장  (0) 2022.04.23
클린코드 6장 (객체와 자료구조)  (0) 2022.04.23
클린코드 4장 (주석)  (0) 2022.04.23