CleanCode 9장 단위테스트

김희윤·2021년 3월 7일

cleancode

목록 보기
9/13

애자일과 TDD 덕택에 단위테스트를 자동화하는 프로그래머들이 많아졌으며 점점 더 늘어나는 추세이다.
제대로 된 좋은 테스트 케이스를 작성하는 것은 아주 중요하다.

1. TDD 법칙 세 가지

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

이렇게 코드를 짠다면 개발과 테스트 주기가 대략 30초 주기로 묶인다.
-> 매일 수십 개, 매달 수백 개, 매년 수천 개에 달하는 테스트 케이스가 나온다.
(실제 코드와 맞먹을 정도로 방대한 테스트 코드는 관리 문제를 야기한다.)

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

테스트 코드가 깔끔하지 않다면 아래와 같은 일이 발생할 수 있다.

  • 새 버전을 출시할 때마다 팀이 테스트 케이스를 유지하고 보수하는 비용이 늘어남
  • 개발자 사이에서 테스트 코드가 가장 큰 불만으로 자리 잡는다.
  • 관리자가 예측값이 너무 큰 이유를 물어보면 팀은 테스트 코드를 비난한다.
  • 테스트 슈트를 폐기한다.
  • 테스트 코드의 부재로 개발자는 수정한 코드가 제대로 도는지 확인할 방법이 없다.
  • 결함율이 높아진다.
  • 의도하지 않은 결함 수가 많아지면 개발자는 변경을 주저한다.
  • 변경하면 득보다 해가 크다 생각해 더 이상 코드를 정리하지 않는다.
  • 코드가 망가진다.
  • 테스트 슈트도 없고, 얼기설기 뒤섞인 코드에, 좌절한 고객과, 테스트에 쏟아 부은 노력이 허사였다는 실망감만 남는다.

-> 테스트 케이스는 변경을 쉽게 할 수 있도록 도와주어 개발의 유연성, 유지보수성, 재사용성을 제공해야 한다.

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

이는 좋지 못한 테스트 코드이다.

  • addPage와 assertSubString을 부르느라 중복되는 코드가 많다.
  • PathParser는 웹 로봇이 사용하는 객체라서 테스트와 무관하며 의도만 흐린다.
  • responder 객체를 생성하는 코드와 response를 수집해 변환하는 코드도 없어도 무관하다.
  • 이 코드를 이해하기 하고 나서야 테스트 케이스를 이해한다.

여기에서는 BUILD-OPERATE-CHECK 패턴이 적합하다
1. 테스트 자료를 만든다.
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");
}

4. 테스트 당 assert 하나

assert 문이 하나인 함수는 결론이 하날서 코드를 이해하기 쉽고 빠르다 하지만 위의 예시는 "출력이 XML"이라는 assert문과 "특정 문자열을 포함한다"는 assert 문이 하나로 병합되어 있다.
쪼개면 아래와 같아진다.

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

	thenResponseShouldContain(
		"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
	); 
}

이렇게 분리를 하다보면 다시 중복되는 코드가 많아진다.
이때 TEMPLATE METHOD를 사용하면 더 쉽게 볼 수 있다.
(given/when 부분은 부모 클래스에 두고)
(then 부분을 자식 클래스에 두는 방식으로)

5. F.I.R.S.T - 깨끗한 테스트 5가지 규칙

  1. 빠르게(Fast) : 테스트는 빨라야 한다.
  • 테스트는 빨리 돌아야 한다.
    - 테스트가 느리면 자주 돌릴 엄두를 못낸다
    - 자주 돌리지 않으면 초반에 문제를 찾아내 고치지 못한다.
    - 코드를 마음껏 정리하지도 못한다.
    - 결국 코드 품질이 망가지기 시작한다.
  1. 독립적으로(Independent) : 각 테스트는 서로 의존하면 안 된다.
  • 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다.
    - 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
    - 테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다.
  1. 반복가능하게(Repeatable) : 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • 실제 환경, QA 환경, 버스를 타고 집으로 가는 길에 사용하는 노트북 환경에서도 실행할 수 있어야 한다.
    - 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
    - 환경이 지원되지 않기에 테스트를 수행하지 못하는 상황에 직면할 수 있다.
  1. 자가검증하는(Self-Validating) : 테스트는 Bool 값으로 결과를 내야 한다. 성공 아니면 실패다.
  • 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안된다.
    - 통과 여부를 보려고 텍스트 파일 두 개를 수작업으로 비교하게 만들어서도 안 된다.
    - 테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다.
  1. 적시에(Timely) : 테스트는 적시에 작성해야 한다.
  • 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
    - 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다.
    - 어떤 실제 코드는 테스트하기 너무 어렵다고 판명날지도 모른다.
    - 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.
profile
블록체인, IOT, 클라우드에 관심이 많은 개발자 지망생

0개의 댓글