1장. 단위 테스트 목표

1장 단위테스트의 목표

단위테스트 현황

단위테스트를 적용해야 하는지는 논쟁 거리조차 아니다. (이미 필수이기 때문에)

문제는 좋은 단위테스트를 작성하려면 어떻게 해야하냐는 것이다.

단위테스트의 목표

단위테스트를 진행하는 목적은 지속 가능한 프로젝트를 만들기 위함이다. 4장에서 다시 언급되지만 지속 가능한 성장이 가능한 메커니즘은 회귀 없이 주기적으로 코드를 리팩터링하고 새로운 기능을 추가할 수 있는 것이다. 테스트 유무에 따라 시간이 흐르면서 프로젝트 진행 속도가 현저히 차이날 수 있다.

이유는 소프트웨어 엔트로피 (무질서도) 때문이다. 소프트웨어 시스템 내의 무질서는 코드에서 비롯된다. 코드베이스에서 변경이 발생하면 무질서도가 증가한다. 적절한 정리와 리팩터링 없이는 시스템이 감당하지 못할 정도로 복잡해지고 무질서해진다. 그리고 테스트를 통해 이러한 경향성을 뒤집을 수 있다.

테스트는 시스템의 안전망 역할을 하며 회귀를 방지하는 역할을 수행한다.

회귀 (Regression) 본 책에서 회귀라는 용어는 버그와 동의어이다. 코드 변경 후 기능이 원하는대로 동작하지 않는 것을 의미

좋은 테스트와 좋지 않은 테스트를 가르는 요인

테스트코드를 작성하는것 만으로는 충분하지 않다. 좋지 않은 테스트코드는 아예 없는것 보다는 낫지만 시간이 흐르면서 시스템의 무질서도가 증가하는것을 막지 못한다. 다음은 일반적인 테스트코드 작성 가이드이다.

  • 베이스코드를 리팩터링하면 테스트코드도 리팩터링 하라

  • 코드의 수정이 발생하면 테스트를 실행하라

  • 테스트가 잘못된 경고를 발생시키면 (거짓 양성) 처리하라

  • 테스트코드를 읽는데 시간을 투자하라

간과하기 쉬운 사실은 테스트코드 또한 코드라는 것이다. 코드는 자산이 아니라 책임이다. 따라서 테스트코드의 양을 늘리는데 집중하면 잠재적인 버그를 발생시킬 가능성이 있고 유지보수 비용이 증가한다.

테스트 커버리지

커버리지 지표는 테스트 스위트 (suite, 스위트룸의 스위트) 가 베이스코드를 얼마나 실행하는지를 백분율로 나타낸 값을 말한다. 일반적으로 커버리지가 높을수록 좋다고 생각되지만 그렇게 간단한 문제는 아니다. 커버리지 지표는 좋은 부정지표이지만 좋지 않은 긍정지표이기 때문이다.

부정지표 vs 긍정지표 본 책에서 부정지표와 긍정지표라는 표현이 자주 등장한다. 말 그대로 부정지표는 좋지 못함을 나타내는 지표이고, 긍정지표는 좋음을 나타내는 지표이다. 테스트 커버리지는 좋은 부정지표이지만 좋지 않은 긍정지표이다. 이 말의 뜻은 커버리지가 낮으면 문제가 있다 (테스트코드가 너무 적다)는 것을 보여주지만 커버리지가 높다고 해서 긍정적인 지표로 해석하기는 어렵다는 의미이다.

많이 사용하는 커버리지 지표로 코드 커버리지와 브랜치 커버리지가 있다.

코드 커버리지

코드 커버리지는 테스트코드가 실행한 제품 코드 수 대비 전체 제품 코드 수의 비율이다.

코드커버리지=실행라인전체라인코드\,커버리지\, = \, \frac {실행\,라인\,수}{전체\,라인\,수}
public boolean isStringLong(String inputStr) {
    if (inputStr.length() > 5) {
        return true;
    }
    return false;
}

@Test
void isStringLong() {
    boolean result = isStringLong("abc");
    assertThat(result)
            .isEqualTo(false);
}

테스트하기를 원하는 isStringLong(String) 메서드의 라인 수는 중괄호 포함 4줄이다. 아래 테스트코드를 실행하면 true를 리턴하는 라인을 제외한 모든 라인을 통과한다. 따라서 이 테스트코드의 커버리지는 3/4로 75%이다. 만약 isStringLong(String) 메서드를 리팩토링하여 한 줄로 만들면 어떻게 될까?

public boolean isStringLong(String inputStr) {
    return inputStr.length() > 5;
}

이제 메서드는 한 줄로 줄었고 테스트코드는 모든 라인을 통과하여 코드 커버리지가 100%가 되었다. 그러나 테스트가 검증하는 결과 개수는 여전히 하나 뿐이다.

분기 커버리지

분기 커버리지는 분기가 존재하는 테스트 대상 코드의 제어문을 통과한 수 대비 전체 제어문의 수의 비율이다.

분기커버리지=실행분기전체분기분기\,커버리지\, = \, \frac {실행\,분기\,수}{전체\,분기\,수}
public boolean isStringLong(String inputStr) {
    return inputStr.length() > 5;
}

@Test
void isStringLong() {
    boolean result = isStringLong("abc");
    assertThat(result)
            .isEqualTo(false);
}

isStringLong(String) 메서드에서 발생 가능한 분기는 입력 문자열의 길이가 5보다 큰 경우와 그렇지 않은 경우이다. isStringLong() 메서드에서는 길이가 5보다 작은 경우만 테스트하고 있으므로 이 경우 분기 커버리지는 50%이다.

커버리지 지표의 문제점

그렇다면 코드 커버리지와 분기 커버리지 만으로 테스트 스위트의 품질을 높일 수 있을까? 그렇지 못하다.

우선 커버리지를 100% 만족시킨다고 해도 테스트코드가 발생 가능한 모든 결과를 검증했다고 보증할 수 없다. 예를 들어 베이스 코드에서 동작을 수행하고 값을 리턴해주는것 이외에 부수적으로 다른 동작을 수행한다고 하자.

private boolean wasLastStringLong;
    
public boolean isStringLong(String inputStr) {
    boolean result = inputStr.length() > 5;
    wasLastStringLong = result;    // 부수적인 동작
    return result;                 // 리턴하는 결과값
}

@Test
void isStringLong() {
    boolean result = isStringLong("abc");
    assertThat(result)
            .isEqualTo(false);     // 테스트코드는 리턴값만 검증
}

@Test
void noValidationTest() {
    boolean result1 = isStringLong("abc");
    boolean result2 = isStringLong("abcdef");
}

위 코드에서 isStringLong(String) 메서드는 wasLastStringLong 이라는 변수를 갱신하는 작업도 수행하고 있다. 하지만 isStringLong() 에서 해당 작업은 검증하지 않고 있다.

다음으로 noValidationTest() 테스트는 isStringLong(String) 의 코드 커버리지와 분기 커버리지를 100% 만족하고 있지만 검증하는 로직이 없다. 아무것도 검증하지 않기 때문에 의미가 없는 테스트코드이다.

이런 문제가 없도록 테스트코드를 잘 작성한다고 해도 문제가 있다. 외부 라이브러리의 코드 경로를 고려할 수 없기 때문이다.

표준 라이브러리를 포함해 직접 작성하지 않은 외부 라이브러리를 사용하는 경우 해당 라이브러리 구현에 의해 발생할 수 있는 수많은 예외 케이스가 있을 수 있다. 우리의 단위테스트 코드에서 이런 모든 예외 상황을 다루고 있는지 확신할 방법이 없다.

물론 단위테스트에서 외부코드의 모든 경로를 고려해서는 안되지만, 어찌되었든 커버리지 지표만으로 테스트가 철저한지는 알 수 없다.

특정 커버리지 숫자를 목표로

앞서 말했듯이 커버리지 지표는 좋은 부정지표이자 나쁜 긍정지표이다. 커버리지가 60% 미만으로 낮다면 문제의 징후이지만 90% ~ 100%라 할 지라도 긍정지표는 아니다. 오히려 너무 높은 지표는 개발자에게 짐을 지우고 프로젝트 완성도에도 도움이 되지 못한다. 70% 정도의 적절한 비율을 목표로 해도 충분하다.

성공적인 테스트 스위트의 특성

개발 주기에 통합

테스트는 개발 주기에 통합되어 있어야 한다. 코드에 변경이 생기면 즉각 테스트를 실행해야 한다.

코드베이스에서 중요한 부분이 대상

모든 코드를 동일한 비중으로 테스트할 필요가 없다. 시스템의 도메인 영역에 가까운 비즈니스 로직 부분은 철저한 검증이 필요하다. 나머지 부분은 간략하게 검증해도 좋다.

최소의 유지비로 최대의 가치를 끌어냄

좋은 단위테스트는 최소한의 유지비를 들여 최대의 가치를 얻는 테스트이다. 이를 위해서는 가치있는 테스트와 그렇지 못한 테스트를 식별할 수 있어야 하며 무엇보다 가치있는 테스트를 작성할 수 있어야 한다.

Last updated