![[Clean Code] 9. 단위 테스트](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxG3Yg%2FbtsFK8CTZls%2FdmGfjovA8Krqin5h3ad501%2Fimg.png)
본 게시글은 도서, Clean Code를 읽고 정리한 글입니다.
Clean Code(클린 코드) | 로버트 C. 마틴 - 교보문고
Clean Code(클린 코드) | 프로그래머, 소프트웨어 공학도, 프로젝트 관리자, 팀 리더, 시스템 분석가에게 도움이 될 더 나은 코드를 만드는 책『Clean Code(클린 코드)』은 오브젝트 멘토(Object Mentor)의
product.kyobobook.co.kr
1. TDD 법칙 세 가지
TDD는 실제 코드를 짜기 전에 단위 테스트부터 짜는 방법론이다.
그렇지만 이 개념은 기본적인 개념일 뿐, 아래 세 가지 법칙을 지켜야 한다.
1) 실패하는 단위 테스트를 작성할 때 까지 실제 코드를 작성하지 않는다.
2) 컴파일은 실패하지 않으면서 실행이 실패하는 정도로 단위 테스트를 작성한다.
3) 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
이렇게 코드를 작성하게 되면, 수많은 테스트 코드들과 실제 코드가 함께 나오게 되고,
실제 전체 코드를 전부 테스트할 수 있는 방대한 테스트 시스템을 갖추게 된다.
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 패턴은 위 테스트 구조에 적합하다. 해당 패턴은 Build, Operate, Check 세 단계로 나누어 각 단계를 순서대로 통과시키는 패턴이다.
첫 단계에서는 테스트 자료를 만든다.
중간 단계에서는 만들었던 테스트 자료를 다루며 조작한다.
마지막 단계에서는 조작했던 실제 결과가 예측 결과와 같은지 확인한다.
이러한 체계적인 구조를 활용하여 군더더기 없이 깔끔한 순서의 테스트 코드를 작성할 수 있다.
또한 위 테스트 코드는 잡다하고 세부적인 코드를 거의 다 없애고, 진짜 본론에 필요한 자료 유형과 함수만을 사용했다,
이러한 방식은 읽는 사람으로 하여금 복잡하고 세부적 코드를 해석하는데 힘쓸 필요가 없이 코드가 수행하는 바를 빠르게 이행할 수 있다.
게다가 위 코드는 도메인에 특화된 언어(DSL, Domain-Specific Language)로, 흔히 사용하는 시스템 조작 API를 사용하는 것이 아니라. API 위에 함수와 유틸리티를 직접 구현한 후 그것들을 사용하기 때문에 테스트 코드를 작성하고 읽기 쉬워진다. 이렇게 작성된 새로운 API는 테스트 코드에서 사용하는 전용 특수 API가 되고, 이는 구현자와 독자의 이해를 돕는다.
이번엔 새로운 아래 두 코드를 보자. 같은 의미를 가지지만 코드의 형식은 다르다.
@Test
public void turnOnLoTempAlarmAtThreashold() throws Esception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
public String getState() {
String state = "";
state += heater ? "H" : "h";
state += blower ? "B" : "b";
state += cooler ? "C" : "c";
state += hiTempAlarm ? "H" : "h";
state += loTempAlarm ? "L" : "l";
return state;
}
@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());
}
우선 첫 번째 테스트 코드만 보자면, 코드를 읽을 때 점검하는 상태 이름과 상태 값 모두를 확인하느라 바쁘다.
무슨 state인지, true인지 false인지 확인해야 한다.
어떻게 보면 가독성이 떨어진다는 것이다. 그리고 그것을 해결한 코드가 두 번째 테스트코드이다.
의미만 안다면 결과를 빠르게 판단할 수 있다.
그리고 getState() 함수의 경우, StringBuffer를 쓰는게 더 효율적일 수 있지만, 가독성은 좋지 않다.
테스트 코드 환경은 실제 코드 환경과 다르게 자원이 제한적일 가능성이 적다.
따라서 약간의 성능을 포기하고 가독성을 높힌다.
이것을 이중 표준 이라고 한다.
실제 환경에서는 절대 안 되지만 테스트 환경에서는 전혀 문제 없고, 오히려 가독성만 더 좋은 방식이다.
4. 테스트 당 assert문 하나
JUnit으로 테스트 코드를 짤 때 하나의 함수에 하나의 assert문만 사용하는 것은 분명한 장점이 있다.
결론이 하나로 도출되기 때문에 테스트 코드를 읽고 이해하기 쉬워진다.
가독성과 이해력이 높아진 테스트 코드의 성능은 위에서 이미 충분히 설명했다!
또, 테스트 코드를 읽기 쉽게 만드는 방법 중 하나는 given-when-then 구조를 사용하는 것이다. 예시는 아래와 같다.
public void testGetPageHierarchyHasRightTags() throws Exception {
// given
givenPages(...);
// when
whenRequestIsIssued(...);
// then
thenResponseShouldContain(...);
}
또 Template Method 패턴을 활용하면 중복 제거가 쉽다.
given / when 부분을 부모 클래스로 두고,. then 부분을 자식 클래스로 두는 것이다.
또는 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given / when 부분을 넣고 @Test 함수에 then 부분을 넣어도 된다.
위에서 assert문을 함수마다 하나씩만 쓰라고 했지만, 어떤 경우에는 assert문을 여러개씩 묶어 사용해도 편한 경우가 있다.
assert문을 한 개만 쓴다는 것은 테스트 함수마다 하나의 개념만 테스트 하라는 뜻이였다.
꼭 한 개만 쓴다기 보다, 하나의 개념만 하나의 함수에서 테스트하자.
"하나의 테스트 함수에 assert문을 최소로 줄여라. (최소가 1개를 의미하는 것은 아니다.)"
"테스트 함수 하나는 개념 하나만 테스트하라."
5. F.I.R.S.T
FIRST는 깨끗한 테스트를 위한 다섯 가지 규칙을 말한다.
1) Fast : 테스트는 빨라야 한다.
테스트는 자주 돌려서 문제를 찾아내야 하는데, 테스트가 만약 느리면 자주 돌릴 엄두가 안난다.
또 테스트를 유지보수할 엄두가 안나게 되고, 결국 망가질 것이다.
2) Independent : 테스트는 다른 테스트에 의존하면 안된다.
한 테스트가 다음 테스트 실행될 환경을 준비해서는 안되고, 테스트를 어떤 순서로 실행해도 상관 없어야 한다.
테스트가 서로 의존하면 하나의 실패가 다른 나머지의 실패를 잇게 된다.
3) Repeatable : 테스트는 어떤 환경 속에서도 반복가능해야 한다.
테스트가 돌아가지 않는 단 하나의 환경이라도 있으면 안된다. 집, 노트북, QA, 버스 등등
4) Self-Validating : 테스트는 bool 값으로서 자가 검증이 가능해야 한다. 즉, 성공 혹은 실패로 결론이 지어져야 한다.
통과 여부를 알기 위해 추가적인 행동이 들어가서는 안된다.
테스트가 테스트 코드로 스스로 성공과 실패를 판가름할 수 없다면, 판단이 결국 주관적이게 된다.
5) Timely : 테스트는 적시에 작성해야 한다.
단위 테스트는 테스트 하려는 실제 코드를 구현하기 직전에 작성한다.
만약 실제 코드 구현 이후에 작성하게 되면, 이미 저질러놓은 실제 코드가 테스트 코드를 작성하기 어렵다는 것을 깨닫게 될 것이다.
'Software Engineering > Clean Code' 카테고리의 다른 글
[Clean Code] 8. 경계 (0) | 2024.02.21 |
---|---|
[Clean Code] 7. 오류 처리 (0) | 2024.02.19 |
[Clean Code] 6. 객체와 자료 구조 (1) | 2024.01.25 |
[Clean Code] 5. 형식 맞추기 (0) | 2024.01.24 |
[Clean Code] 4. 주석 (1) | 2024.01.23 |
개발자가 되고 싶어요.