단위 테스트 현황

지난 20년간 단위 테스트를 적용할 것을 독려하는 분위기가 자리 잡았다. 모든 새로운 기술과 마찬가지로 단위 테스트도 계속 발전하고 있다. 논쟁은 '단위 테스트를 작성해야 하는가?'에서 '좋은 단위 테스트를 작성하는 것은 어떤 의미인가?'로 바뀌었다. 이는 여전히 매우 혼란스럽다. 

많은 프로젝트에 자동화된 테스트가 있고, 심지어 테스트도 많다. 그러나 테스트가 있어도 개발자들이 원하는 결과를 얻지 못하는 경우가 많다. 단위 테스트는 도움을 약속했으나 상황을 더 나쁘게 할 수 있다. 따라서 어떤 것이 단위 테스트를 좋게 만드는지에 대한 논쟁은 매우 중요하다. 

 

단위 테스트의 목표

단위 테스트와 코드 설계의 관계

코드 조각을 단위 테스트하는 것은 훌륭한 리트머스 시험이지만, 한 방향으로만 작동한다. 이는 괜찮은 부정 지표다. 즉, 비교적 높은 정확도로 저품질 코드를 가려낸다. 

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

코드베이스에서 무언가를 변경할 때마다 무질서도(엔트로피)는 증가한다. 지속적인 정리와 리팩터링 등과 같은 적절한 관리를 하지 않고 방치하면 시스템이 점점 더 복잡해지고 무질서해진다. 하나의 버그를 수정하면 더 많은 버그를 양산하고, 소프트웨어의 한 부분을 수정하면 다른 부분들이 고장 난다. 즉, 도미노 현상과 같다. 최악의 경우 안정되게 복구하는 것은 어렵다. 

테스트로 이러한 경향을 뒤집을 수 있다. 테스트는 안전망 역할을 하며, 대부분의 회귀에 대한 보험을 제공하는 도구라 할 수 있다. 

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

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

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

✔️ 기반 코드를 리팩터링할 때 테스트도 리팩터링 하자.
✔️ 각 코드 변경 시 테스트를 실행하라. 
✔️ 테스트가 잘못된 경고를 발생시킬 경우 처리하라.
✔️ 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는 데 시간을 투자하라.

테스트 스위트 품질 측정을 위한 커버리지 지표

커버리지 지표는 중요한 피드백을 주더라도 테스트 스위트 품질을 효과적으로 측정하는 데 사용될 수 없다. 코드를 단위 테스트하는 것과 같은 상황이다. 즉, 커버리지 지표는 괜찮은 부정 지표이지만 좋지 않은 긍정 지표다😱
예를 들어, 코드 커버리지가 너무 적을 때는 테스트가 충치 않다는 증거이지만, 100%라고 해서 반드시 양질의 테스트 스위트라고 보장하지는 않는다. 높은 커버지리의 테스트 스위트도 품질이 떨어질 수 있다. 

코드 커버리지 지표에 대한 이해

아래 두 코드 테스트 코드를 보며 대해 코드 커버리지를 생각해보자. 

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

첫 번째 코드는 전체 라인 3줄 중에 `return true`를 제외한 모든 코드 라인을 통과한다. 따라서, 코드 커버리지는 2/3 = 0.66 = 60%다. 
하지만 두 번째 코드를 확인해보면 커버리지가 100%로 증가한 것을 알 수 있다. 

이 간단한 예제는 커버리지 숫자에 대해 얼마나 쉽게 장난칠 수 있는지🤪 보여준다. 

분기 커버리지 지표에 대한 이해

또 다른 커버리지 지표는 분기 커버리지(branch coverage)이다. 분기 커버리지는 코드 커버리지의 단점을 극복하는 데 도움이 되므로 코드 커버리지보다 더 정확한 결과를 제공한다. 분기 커버리지 지표는 분기 개수만 다루며, 해당 분기를 구현하는 데 얼마나 코드가 필요한지 고려하지 않는다. 

커버리지 지표에 관한 문제점

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

 

가능한 모든 결과를 검증한다고 보증할 수 없음

테스트 대상 시스템이 낸 결과가 정확히 예상하는 결과인지 확인해야 한다. 더구나 결과가 여러 개 있을 수 있다. 따라서 커버리지 지표가 의미가 있으려면, 모든 측정 지표를 검증해야 한다. 

아래 코드를 살펴보자. 

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); // 두 번 째 결과만 검증
}

위의 코드에서 살펴볼 수 있듯이 한 함수에서 두 결과를 가지고 있음에도 불구하고 한 결과에 대해서만 테스트하는 것을 확인할 수 있다. 그럼에도 불구하고, 테스트 커버리지는 100%이다. 그러나 아무것도 검증하지 않기 때문에 전혀 쓸모가 없다. 

 

외부 라이브러리의 코드 경로를 고려할 수 없음

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

다음 예제를 살펴보자. 

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 메서드는 `null`, `""`, "5"`, `정수가 아님` 과 같은 결과로 이어질 수 있다. 사실상 외부 라이브러리의 케이스를 한 개밖에 커버하지 못했다. 

수많은 예외 상황에 빠질 수 있지만, 테스트에서 모든 예외 상황을 다루는지 확인할 방법이 없다. 

 

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

이제 테스트 스위트 푸질을 결정하기에 커버리지 지표만으로는 충분치 않다는 것을 알길 바란다. 특정 커버리지 숫자를 목표로 삼기 시작하면 위험 영역으로 이어질 수 있다. 커버리지 지표를 보는 가장 좋은 방법은 지표 그 자체를 보는 것이며, 목표로 여겨서는 안 된다. 

커버리지 숫자를 목표로 하는 것은 단위 테스트의 목표와 반대되는 그릇된 동기 부여가 된다. 사람들은 테스트하는데 집중하는 대신 인공적인 목표를 달성하기 찾기 시작한다. 커버리지 숫자를 강요하면 개발자들은 테스트 대상에 신경 쓰지 못하고, 결국 적절한 단위 테스트는 더욱 달성하기 어려워진다. 

무엇이 성공적인 테스트 스위트를 만드는가?

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

 

개발 주기에 통합돼 있음

코드가 변경될 때마다 아무리 작은 것이라도 테스트 코드를 실행해야 한다. 

 

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

모든 테스트가 똑같이 작성되지 않은 것처럼 단위 테스트 측면에서 코드베이스의 모든 부분에 똑같이 주목할 필요는 없다. 대부분의 애플리케이션에서 가장 중요한 부분은 비즈니스 로직이 있는 부분이다. 비즈니스 로직 테스트가 시간 투자 대비 최고의 수익을 낼 수 있다. 

 

✔️ 인프라 코드
✔️ 데이터베이스나 서드파티 시스템과 같은 외부 서비스 및 종속성
✔️ 모든 것을 하나로 묶는 코드

 

인프라 코드에는 복잡하고 중요한 알고리즘이 있을 수 있으므로, 테스트를 많이 하는 것이 좋다. 그러나 일반적으로 도메인 모델에 관심을 더 많이 갖는 것이 옳다. 통합 테스트도 코드베이스의 중요하지 않은 부분을 포함해 시스템의 전체 동작을 테스트할 수 있지만 초점은 도메인 모델에 있어야 한다. 

이 지침에 따르면 도메인 모델을 코드베이스 중 중요하지 않은 부분과 분리해야 한다. 이것은 2부에서 설명한다. 

 

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

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

 

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

 

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

 

반응형

'Testing' 카테고리의 다른 글

Unit Testing 2-1장 :: '단위 테스트'의 정의  (0) 2022.11.07
복사했습니다!