클린코드(CleanCode)를 읽고 간략하게 정리한 글입니다.
9장. 단위 테스트
- 우리들 대다수에게 단위 테스트란 자기 프로그램이 '돌아간다'는 사실만 확인하는 일회성 코드에 불과했다.
- 에자일과 TDD 덕택에 단위 테스트를 자동화하는 개발자들이 이미 많아졌으며 점점 더 늘어나는 추세이다.
- 테스트를 추가하려고 급하게 서두르는 와중에 제대로 된 테스트 케이스를 작성해야 한다는 더욱 중요한 사실을 놓치고 있다.
1. TDD 법칙 세 가지
- TDD(Test Driven Development) : 테스트 주도 개발(실제 코드를 작성하기 전에 단위 테스트부터 작성하라)
- 첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
- 위 규칙을 따라 일을 한다면 매일 수백, 수천 개에 달하는 테스트 케이스가 나온다.
- 실제 코드를 사실상 전부 테스트하는 테스트 케이스가 나온다.
- 그렇지만 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
2. 깨끗한 테스트 코드 유지하기
- 테스트 코드가 지저분할 경우 실제 코드가 변경 시에 원활하게 대응할 수 없게 된다.
- 그러면 실제 코드 변경시 실제 코드를 짜는 시간보다 테스트 케이스 수정에 더 많은 시간이 걸린다.
- 이는 테스트 케이스를 점점 더 통과시키기 어려워지며 결국 테스트 코드를 폐기해야 하는 상황이 발생하게 된다.
- 결국 코드 수정 시 다른 쪽이 안전하다는 사실을 검증할 수 없게 되어 변경을 주저하게 되고 그러면서 코드는 지저분해진다.
- 테스트 코드는 실제 코드 못지않게 중요하다.
- 테스트 코드는 사고와 설계와 주의가 필요하다. 실제 코드 못지 않게 깨끗하게 짜야한다.
2-1. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.
- 테스트 코드를 깨끗하게 유지하지 않으면 결국은 잃어버린다.
- 테스트 코드가 지저분하면 코드를 변경하는 능력이 떨어지며 코드 구조를 개선하는 능력도 떨어진다.
- 테스트 케이스가 없으면 실제 코드를 유연하게 만드는 버팀목이 사라진다.
- 테스트가 없으면 모든 변경이 잠정적인 버그이다.
- 아키텍처가 아무리 유연하더라도 테스트 케이스가 없으면 개발자는 변경을 주저한다.
- 테스트 케이스가 있다면 공포는 사실상 사라진다.
- 테스트 케이스가 검증을 해주기 때문에 코드 변경이 쉬워진다.
- 코드에 유연성, 유지 보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다.
3. 깨끗한 테스트 코드
- 깨끗한 테스트 코드를 만들려면? 세 가지가 필요하다. -> 가독성! 가독성! 가독성!
- 어쩌면 실제 코드보다 가독성이 더 중요하다.
- 이를 위해 명료성, 단순성, 풍부한 표현력이 필요하다.
- 테스트 코드에는 최소의 표현으로 최대한 많은 것을 나타내야 한다.
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);
}
- 위 코드를 살펴보면 자질구례 한 사항이 너무 많아 테스트 코드의 표현력이 떨어진다.
- 위 코드는 읽는 사람을 고려하지 않는다. 독자들은 온갖 잡다하고 무관한 코드를 이해한 후에야 간신히 테스트 케이스를 이해한다.
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-1. 도메인에 특화된 테스트 언어
- 바로 위의 코드는 도메인의 특화된 언어(DSL)로 테스트 코드를 구현하는 기법을 보여준다.
- 테스트를 구현하는 당사자와 나중에 테스트를 읽어볼 독자를 도와주는 테스트 언어이다.
- 이러한 테스트 API는 처음부터 설계된 API가 아니다. 잡다하고 세세한 사항으로 범벅된 코드를 계속 리팩터링 하다가 진화된 API이다.
3-2. 이중 표준
- 테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.
- 단순하고, 간결하고 표현력이 풍부해야 하지만, 실제 코드만큼 효율적인 필요는 없다. -> 테스트 환경에서 돌아가는 코드이기 때문이다.
// 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 turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
4. 테스트 당 assert 하나
- 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 사람들도 있다.
- 가혹한 규칙이라 여길지도 모르지만 확실히 장점이 있다.
- assert 문이 단 하나인 함수는 결론이 하나이기 때문에 코드를 이해하기 쉽고 빠르다.
- 개념이 같다면 한 함수에 assert를 여러 개 사용해도 괜찮다.
- 단지 assert 문의 개수를 최대한 줄여야 한다는 것에는 동의한다.
- 테스트 함수마다 한 개념만 테스트해야 한다.
4-1. 테스트당 개념 하나
- 위의 테스트 당 assert 하나 보다는 테스트 함수마다 한 개념만 테스트하라 라는 규칙이 더 낫다.
- 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피한다.
- 가장 좋은 규칙은 "개념 당 assert 문 수를 최소로 줄여라"와 "테스트 함수 하나는 개념 하나만 테스트하라"라 이다.
5. F.I.R.S.T
- 깨끗한 테스트는 다음 다섯 가지 규칙을 따르는데, 각 규칙에서 첫 글자를 따오면 FIRST 가 된다.
- 빠르게(Fast)
- 테스트는 빨라야 한다. -> 테스트는 빨리 동작해야 한다는 말이다.
- 테스트가 느리면 자주 돌릴 엄두를 못 낸다.
- 자주 돌리지 않으면 초반에 문제를 찾아내 고치지 못한다.
- 코드를 마음껏 정리하지도 못한다. -> 결국 코드 품질이 망가지기 시작한다. - 독립적으로(Independent)
- 각 테스트를 서로 의존하면 안 된다.
- 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다.
- 각 테스트를 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
- 테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다. - 반복가능하게(Repeatable)
- 테스트는 어떤 환경에서도 반복 가능해야 한다.
- 실제 환경, QA 환경, 버스를 타고 집으로 가는 길에 사용하는 노트북 환경(네트워크가 연결되지 않은)에서도 실행할 수 있어야 한다.
- 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
- 게다가 환경이 지원되지 않기에 테스트를 수행하지 못하는 상황에 직면한다. - 자가검증하는(Self-Validating)
- 테스트는 bool 값으로 결과를 내야 한다. -> 성공 아니면 실패이다.
- 통과 여부를 알리고 로그 파일을 읽게 만들어서는 안 된다.
- 통과 여부를 보려고 텍스트 파일 두 개를 수작업으로 비교하게 만들어서도 안된다.
- 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다. - 적시에(Timely)
- 테스트는 적시에 작성해야 한다. -> 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
- 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다.
- 어떤 실제 코드는 테스트하기 너무 어렵다고 판명 날지 모른다.
- 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.
5. 결론
- 테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다.
- 어쩌면 실제 코드보다 중요할 수 있다.
- 테스트 코드는 실제 코드의 유연성, 유지 보수성, 재사용성을 보존하고 강화하기 때문이다.
- 테스트 코드를 지속적으로 깨끗하게 관리하라 -> 표현력을 높이고 간결하게 정리하라.
'Study > CleanCode' 카테고리의 다른 글
[클린코드] 11장. 시스템 (0) | 2023.11.15 |
---|---|
[클린코드] 10장. 클래스 (2) | 2023.11.12 |
[클린코드] 8장. 경계 (1) | 2023.11.04 |
[클린코드] 6장. 객체와 자료 구조 (0) | 2023.10.25 |
[클린코드] 5장. 형식 맞추기 (0) | 2023.10.22 |