개발/단위테스트

[단위테스트] 1장 단위테스트의 목표

Leedo1982 2022. 2. 19. 10:15

테스트에 대한 논쟁은 '단위테스트를 작성해야하는가?'에서 '좋은 단위테스트를 작성하는 것은 어떤 의미인가?' 로 바뀌었다.

 

단위테스트의 목표는 무엇일까?

흔히 단위테스트 활동이 더 나은 설계로 이어진다고 한다. 이는 사실이다. 더 나은 설계는 단지 좋은 부수효과일 뿐이다.

 

목표는 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다. 지속가능하다는 것이 핵심이다.

프로젝트가 상당히 쉽게 성장할 수 있다. 특히 처음부터 시작할때 그렇다. 하지만 시간이 지나면서 이렇게 계속 성장하기는 어렵다.

https://user-images.githubusercontent.com/12313132/145699260-837f6523-6bc0-4c97-846b-6c12d7b06a7e.png

(테스트 유무에 따른 프로젝트 간 성장추이의 차이. 테스트가 없는 프로젝트의 경우 시작은 유리하지만 이내 진척이 없을 정도로 느려진다.:::너무나도 뻔한 그래프)

 

개발속도가 빠르게 감소하는 이러한 현상을 소프트웨어 엔트로피라고도 한다.

단위테스트가 프로젝트 성장에 도움이 되는 것은 맞지만, 테스트를 작성하는 것만으로는 충분하지 않다. 잘못 작성한 테스트는 여전히 같은 결과를 낳는다.

https://user-images.githubusercontent.com/12313132/145699352-698179a9-8665-449b-9c52-3ad265e5d841.png

(테스트가 좋은지 나쁜지에 따른 프로젝트가 성장 추이의 차이. 테스트가 잘못 작성된 프로젝트는 초반에는 테스트가 잘 작성된 프로젝트의 속성을 보여주지만, 결국 침체 단계에 빠진다.)

 

모든 테스트를 작성할 필요는 없다. 일부테스트는 아주 중요하고 소프트웨어 품질에 매우 많은 기여를 한다. 그밖에 다른 테스트트 그렇지 않다. 잘못된 경고가 발생하고,회기오류를 알아내는 데 도움이 되지 않으며,유지보수가 어렵고 느리다.

 

프로젝트에 도움이 되는지 여부를 명확하게 파악하지 않고 단위테스트를 작성하는 데만 빠져들기 쉽다.

높은 유지보수 비용으로 인해 순가치가 0에 가깝거나 심지어 0보다 작은 테스트를 만들기 쉽다. 지속가능한 프로젝트 성장을 위해서는 고품질 테스트에만 집중해야한다. 고품질 테스트만이 테스트 스위트에 남을 만한 테스트 유형이다.(좋은 단위테스트와 나쁜 단위테스트를 구별하는방법은 4장에서)

 

테스트 스위트 품질측정을 위해 가장 널리 사용되는 커버리지 지표를 살펴보자.

커버리지 지표는 각기 다른 유형이 있으며, 테스트 스위트의 품질을 평가하는데 자주 사용된다.

일반적으로 숫자가 높을수록 더좋다?

단순히 그렇지는 않다. 커버리지 지표는 중요한 피드백을 주더라도 테스트 스위트 품질을 효과적으로 측정하는데 사용될 수 없다. 즉, 커버리지 지표는 괜찮은 부정지표이지만 좋지 않은 긍정지표다.

우선 가장 많이 사용되는 커버리지 지표로 코드 커버리지가 있다.

$코드커버리지(테스트 커버리지) = 제품 코드라인수 / 전체라인 수$

public static bool IsStringLong(    string input)
{
    if(input.Length > 5)
        return true;
    return false;
}

public void Test()
{
    bool result = IsStringLong("abc");
  Assert.Equal(false, result);
}
public static bool IsStringLong(string input)
{
    return input.Length > 5;
}

public void Test()
{
    bool result = IsStringLong("abc");
  Assert.Equal(false, result);
}

단지 메서드 내 코드를 바꿨을뿐이다. 이 테스트가 검증하는 결과 개수는 여전히 같다.

 

이 간단한 예제는 커버리지 숫자에 대해 얼마나 쉽게 장난칠 수 있는지 보여준다. 코드가 작을수록 테스트 커버리지 지표는 더 좋아지는데, 이는 원래 라인 수만 처리하기 때문이다. 그리고 코드를 더 작게해도 테스트 스위트의 가치나 기반 코드베이스의 유지보수성이 변경되지 않는다.

 

또다른 커러비리 지표는 분기 커버리지다. 분기 커버리지는 코드 커버리지의 단점을 극복하는 데 도움이 되므로 코드 커러비리지 보다 더 정확한 결과를 제공한다.

 

    $분기 커버리지 = 통과분기 / 전체분기 수$

 

분기 커러리지 지표를 계산하려면 코드베이스에서 모든 가능한 분기를 합산하고 그중 테스트가 얼마나 많이 실행되는지 확인해야한다. 이전의 예를 다시보면 분기 커버리지는 변경되지 않는다.

분기 커버리지로 코드 커버리지보다 더 나은 결과를 얻을 수 있지만, 테스트 스위트의 품질을 결정하는 데 어떤 커버리지 지표도 의존할 수 없는 이유는 다음과 같다.

  • 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다.
  • 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다.

 

"테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다."

 

public static bool WasLastStringLong { get; private set; }

public static bool IsStringLong(string input)
{
    bool result = input.Length > 5;
    WasLastStringLong = result ; <- 첫번째 결과
    return result;  <- 두번째 결과
}

public void Test()
{
    bool result = IsStringLong("abc");
    Assert.Equal(false, result); <- 두번째 결과만 검증
}

보다시피 커버리지 지표는 기반 코드를 테스트했다고 보장할 수 없으며 일부 실행된 것만 보장한다.

더 극단적인 상황은 검증이 전혀 없는 테스트의 경우다.

public void Test()
{
    bool result1 = IsStringLong("abc");
    bool result2 = IsStringLong("abcdef");
}

"외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다."

모든 커버리지 지표가 테스트 대상 시스템이 메서드를 호출할 때 외부 라이브러리가 통과하는 코드 경로를 고려하지 않는다는 것이다.

public static int Parse(string input)
{
    return int.Parse(input);
}

public void Test()
{
    int result = Parse("5");
    Assert.Equal(5, result);
}

분기 커버리지 지표는 100%로 표시되며 테스트 메서드 결과의 모든 구성요소를 검증한다. 단지 값을 반환하는 한줄이라 하더라도 단일한 구성요소이기는 하다. 하지만 이 테스트틑 완벽하지 않다. .NET 프레임워크의 int.Parse 메서드가 수행하는 코드 경로는 고려하지 않았다.

https://user-images.githubusercontent.com/12313132/145700034-21e9f2aa-4680-4e3a-b424-d5bb984e6cd3.png

(외부라이브러리의 숨겨진 코드 경로. 커버리지 지표는 얼마나 있는지, 그리고 테스트가 얼마나 수행하는지 알 방법이 없다)

이는 커버리지 지표가 외부 라이브러리의 코드 경로를 고려해야 한다는 것이 아니라(고려하면 안된다). 해당 지표로는 단위 테스트가 얼마나 좋은지 나쁜지를 판단할 수 없다는 것을 보여준다. 커버리지 지표로 테스트가 철저한지 또는 테스트가 충분한지 알 수는 없다.

커버리지 지표를 보는 가장 좋은 방법은 지표 그 자체로 보는 것이며, 목표로 여겨서는 안된다. 특정 커버리지 숫자를 목표하는 것은 단위 테스트의 목표와 반대되는 그릇된 동기부여가 된다. 사람들은 중요한 것을 테스트하는 데 집증하는 대신 인공적인 목표를 달성하기 위한 방법을 찾기 시작한다.

다시 말하면, 커버리지 지표는 좋은 부정지표이지만 나쁜 긍정지표이다. 커버리지 숫자가 낮으면(예: 60%미만) 문제 징후라 할 수 있다. 코드베이스에 테스트되지 않은 코드가 많다는 뜻이다. 그러나 높은 숫자도 별의미는 없다. 그러므로 코드커버리지를 측정하는 것은 품질 테스트 스위트로 가는 첫걸음일 뿐이다.

전체적으로 어떻게 테스트 스위트를 성공할 수 있는지 더 넓게 살펴보자(4장에서 좋은 테스트와 좋지 않은 테스트를 구분하는 세부사항을 자세히 살펴볼것이다)

 

성공적인 테스트 스위트는 다음과 같은 특성을 갖고 있다.

1. "개발 주기에 통합돼 있다"

자동화된 테스트를 할 수 있는 방법은 끊임없이 하는 것뿐이다. 모든 테스트는 개발주기에 통합돼야 한다. 이상적으로는 코드가 변경될 때마다 아무리 작은 것이라도 실행해야한다.

 

2. "코드베이스에서 가장 중요한 부분만을 대상으로 한다"

모든 테스트가 똑같이 작성되지 않는 것처러 단위 테스트 측면에서 코드베이스의 모든 부분에 주목할 필요는 없다. 테스트가 주는 가치는 테스트 구조뿐만 아니라 검증하는 코드에도 있다.

시스템의 가장 중요한 부분에 단위 테스트 노력을 기울이고, 다른 부분은 간략하게 또는 간접적으로 검증하는 것이 좋다. 대부분의 애플리케이션에서 가장 중요한 부분은 비지니스로직(도메인모델)이 있는 부분이다. 비지니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다.

 

3. "최소한의 유지비로 최대의 가치를 이끌어 낸다."

단위테스트에서 가장 어려운 부분은 최소 유지비로 최대 가치를 달성하는 것이다.

테스트를 빌드 시스템에 통합하는 것만으로는 충분하지 않으며, 도메인 모델에 높은 테스트커버리지를 유지하는 것도 충분하지 않다. 또한 가치가 유지비를 상회하는 테스트만 스위트에 유지하는 것이 중요하다.

이 마지막 속성은 두가지로 나눌수 있다.

  • 가치 있는 테스트(더나아가, 가치가 낮은 테스트)식별하기
  • 가치 있는 테스트 작성하기

 

가치가 높은 테스트를 식별하려면 기준틀(frame or reference)이 필요하다. 반면 가치 있는 테스트를 작성하려면 코드 설계 기술도 알아야한다. 단위테스트와 기반 코드는 서로 얽혀 있으므로 코드베이스에 노력을 기울이지 않으면 가치있는 테스트를 만들 수 없다. 좋은 곡을 식별하는 것과 작곡할 수 있는 것의 차이로 볼 수 있다.