GO SIWOO!

[클린 코드] 9장 - 단위 테스트 본문

Develop/클린코드

[클린 코드] 9장 - 단위 테스트

gosiwoo 2023. 9. 19. 03:12
 

[클린 코드] 8장 - 경계

1. 외부 코드 사용하기 패키지 또는 프레임워크 사용자는 자신의 요구에 집중하는 인터페이스를 바란다 -> 시스템 경계에서 문제가 생길 수 있다. // case1 Map sensors = new HashMap(); Sensor s = (Sensor)sensor

gosiwoo.tistory.com

1. TDD의 법칙 세 가지

1. 첫째 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

2. 둘째 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.

3. 셋째 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

이렇게 하면 수 많은 테스트 코드를 만들어 낸다. 하지만 이는 심각한 관리 문제를 유발하기도 한다.

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

지저분한 테스트 코드는 오히려 안 내놓으니만 못하다.

테스트 코드는 지저분할수록 변경하기 어려워진다. 

테스트 코드는 실제 코드 못지 않게 중요하다. 

 

코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 단위 테스트이다.

테스트 커버리가 높을수록 공포가 줄어든다.

3. 깨끗한 테스트 코드

꺠끗한 테스트 코드를 만르려면 세 가지가 필요하다. 가독성, 가독성, 가독성. 가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다.

// case 9-1 SerializedPageResponderTest.java
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.getContent();

  assertEquals("text/xml", response.getContentType());
  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"));
  crawler.addPage(root, PathParser.parse("PageTwo"));

  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.getContent();

  assertEquals("text/xml", response.getContentType());
  assertSubString("test page", xml);
  assertSubString("<Test", xml);
}
// case 9-2 SerializedPageResponderTest.java (리팩토링한 코드)
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");
}

case 9-2는 case 9-1의 잡다하고 세세한 코드를 거의 다 없앴다. 도메인에 특화된 테스트 언어(DSL)로 테스트 코드를 구현하는 기법을 보여준다.

 

다음과 같은 예제로 가독성을 높인 경우도 있다.

// case 9-3
@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());
}
// case 9-4
@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()); 
}
...

9-3 case는 assertTrue, assertFalse 등 뒤죽박죽 섰여있다.

이를 9-4의 case로 단순하게 바꾸어 가독성을 높였다. tic() 함수는 wayToocold라는 함수를 만들어 숨기고 검증 목록들중 assertTrue(hw.heaterState())만 남겨 assertEquals로 검증하였다.

 

이는 테스트 코드는 간결해 졌지만 검증 요소 나머지들을 추가적으로 작성해 주어야 한다. 하지만, 테스트 코드는 실제 코드와 실행환경이 다르다. 메모리와 CPU의 효율과는 무관하다. 이것을 이중 표준이라고 부른다. 

 

불변성을 갖고 있는 String 대신 성능 때문에 StringBuffer를 사용하면 좋을지언정 코드의 가독성이 떨어지므로 테스트 코드에서 사용을 지양하는 것과 같다.

4. 테스트 당 assert 하나

JUnit으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용한다면 결론이 하나기에 코드를 이해하기 빠르고 쉽다. assert문을 2개 이상 써야 할 경우는 어떻게 할까? 2개 이상으로 분할하여 수행하면 된다.

하지만 코드를 분리하면 중복되는 코드가 많아지는데 이는 템플릿 메서드 패턴을 사용하면 되지만 이는 비효율 적이므로 차라리 돌고돌아 assert 문을 여럿 사용하는게 좋다.

 

테스트 당 assert 하나 -> 테스트 당 개념 하나

테스트 당 개념 하나를 테스트 하는 것을 목표로 하는 것이 좋겠다.

5. F.I.R.S.T

깨끗한 테스트의 다섯 가지 규칙을 따르는데, 각 규칙의 첫글자를 따오면 FIRST가 된다.

 

Fast 빠르게

테스트는 빨라야 한다. 느린 테스트는 자주 돌리지 못하고 문제를 찾아내지 못한다. 이는 결국 코드 품질이 망가지기 시작한다.

 

Independent 독립적으로

각 테스트는 서로 의존하면 안 된다. 테스트의 실행 순서를 준비해서도 안된다. 모든 테스트는 독립적으로 어떤 순서에 실해외어도 괜찮아야 한다. 테스트가 서로에게 의존하면 연쇄적으로 실패하기 떄문에 결함을 찾기가 어려워진다.

 

Repeatable 반복가능하게

테스트는 어떤 환경에서도 반복 가능해야 한다. 네트워크에 연결되지 않은 환경, 집에가는 길에, QA환경 모두 실행될 수 있어야 한다. 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트 실패의 변명이 생긴다.

 

Self-Validating 자가검증하는

테스트는 bool 값으로 결과를 내야 한다. 성공 또는 실패, 통과 여부를 알기 위해 로그를 열어서는 안 된다.

테스트가 스스로 성공과 실패를 가늠할 수 있어야 한다.

 

Timely 적시에

테스트는 적시에 작성해야 한다. 테스트는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어려워질 수 있다. 추가로 테스트가 불가능하게 테스트 코드를 작성할지도 모른다.

'Develop > 클린코드' 카테고리의 다른 글

[클린 코드] 11장 - 시스템  (0) 2023.09.27
[클린 코드] 10장 - 클래스  (0) 2023.09.19
[클린 코드] 8장 - 경계  (0) 2023.09.19
[클린 코드] 4장 - 주석  (0) 2023.08.29
[클린 코드] 3장 - 함수  (0) 2023.07.21