🐤 Chapter 1: 단위 테스트의 목표
단위 테스트에 시간을 투자할 때는 항상 최대한 이득을 얻도록 노력해야하며, 테스트에 드는 노력을 가능한 한 줄이고 그에 따른 이득긍 최대화해야 한다.
🥕 단위 테스트 현황
모든 새로운 기술과 마찬가지로 단위 테스트도 계속 발전하고 있다. 논쟁은 "단위 테스트를 작성해야 하는가?"에서 "좋은 단위 테스트를 작성하는 것은 어떤 의미인가?"로 바뀌었다. 이는 아직도 혼란스러운 상태다.
🥕 단위 테스트의 목표
흔히 단위 테스트 활동이 더 나은 설계로 이어진다고 한다. 이는 사실이다. 코드베이스에 대해 단위 테스트 작성이 필요하면 일반적으로 더 나은 설계로 이어진다. 하지만 단위 테스트의 주목표는 아니다. 더 나은 설계가 단지 좋은 부수 효과일 뿐이다.
그럼 단위 테스트의 목표는 무엇인가? 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것이 핵심이다. 지속 가능하다는 것이 핵심이다. 프로젝트가 상당히 쉽게 성장할 수 있다. 특히 처음부터 시작할 때 그렇다. 하지만 시간이 지나면서 이렇게 계속 성자하기는 훨씬 어렵다.
개발 속도가 빠르게 감소하는 이러한 현상을 소프트웨어 엔트로피(시스템 내 무질서도)라고도 한다. 지속적인 정리와 리팩터링 등과 같은 적절한 관리를 하지 않고 방치하면 시스템이 점점 더 복잡해지고 무질서해진다.
테스트로 이러한 경향을 뒤집을 수 있다. 테스트는 안전망 역할을 하며, 대부분의 회귀에 대한 보험을 제공하는 도구라 할 수 있다. 테스트는 새로운 기능을 도입하거나 새로운 요구 사항에 더 잘 맞게 리팩터링한 후에도 기존 기능이 잘 작동하는지 획인하는 데 도움이 된다.
여기서 한 가지 단점을 생각해볼 수 있는데, 이러한 테스트는 초반에 노력(어쩌면 상당한 노력)이 필요하다는 것이다. 그러나 프로젝트 후반에도 잘 성장할 수 있도록 하므로 장기적으로 보면 그 비용을 메울 수 있다. 코드베이스를 지속적으 로 검증하는 테스트 없이는 소프트웨어 개발이 쉽게 확장되지 않는다.
지속성과 확장성이 핵심이며, 이를 통해 장기적으로 개발 속도를 유지할 수 있다.
🎈 좋은 테스트와 좋지 않은 테스트를 가르는 요인
잘못 작성한 테스트는 여전히 같은 결가를 낳는다. 잘못 작성한 테스트도 초반에 코드가 나빠지는 것을 늦출 수 있다. 즉, 테스트가 전혀 없는 상황에 비해 개발 속도가 덜 느려진다. 그러나 거시적인 관점에서는 큰 차이가 없다. 이러한 프로젝트가 침체 단계에 진입하는 데 시간이 더 걸릴 수 있지만, 피할 수는 없다.
프로젝트에 테스트를 더 많이 실행하더라도 단위 테스트의 목표를 달성할 수 없다. 테스트의 가치와 유지 비용을 모두 고려해야 한다. 비용 요소는 다음과 같은 다양한 활동에 필요한 시간에 따라 결정된다.
- 기반 코드를 리팩터링할 때 테스트도 리팩터링하라.
- 각 코드 변경 시 테스트를 실행하라.
- 테스트가 잘못된 경고를 발생시킬 경우 처리하라.
- 기반 코드가 어떻게 동작하는지 이해하려고 할 때는 테스트를 읽는 데 시간을 투자하라.
지속 가능한 프로젝트 성장을 위해서는 고품질 테스트에만 집중해야 한다. 고품질 테스트만이 테스트 스위트에 남을 만한 테스트 유형이다.
🥕 테스트 스위트 품질 측정을 위한 커버리지 지표
정의: 커버리지 지표는 테스트 스위트가 소스 코드를 얼마나 실행하는지를 백분율로 나타낸다.
코드 커버리지가 너무 적을 때는 테스트가 충분치 않다는 좋은 증거다. 그러나 반대의 경우는 그렇지 못하다. 100% 커버리질고 해서 반드시 양질의 테스트 스위트라고 보장하지는 않는다. 높은 커버리지의 테스트 스위트도 품질이 떨어질 수 있다.
🎈 코드 커버리지 지표에 대한 이해
코드 커버리지(테스트 커버리지)는 하나 이상의 테스트로 실행된 코드 라인 수와 제품 코드베이스의 전체 라인 수의 비율을 나타낸다.
코드 커버리지(테스트 커버리지) = 실행 코드 라인 수 / 전체 라인 수
// 테스트 커버리지 4/5 = 0.8 = 80%
public static bool IsStringLong(string input) {
if (input.Length > 5)
return true; // 아래 테스트가 다루지 않는 영역
return false;
}
// test
public void Test() {
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
// 리팩터링 테스트 커버리지 100%
// but, 테스트를 검증하는 결과 개수는 어젼히 똑같다.
public static bool IsStringLong(string input) {
return input.Length > 5;
}
코드가 작을수록 테스트 커버리지 지표는 더 좋아지는데, 이는 원래 라인 수만 처리하기 때문이다. 그리고 코드를 더 작게 해도 테스트 스위트의 가치나 기반 코드베이스 유지 보수성이 변경되지 않는다.(변경해서도 안 된다.)
🎈 분기 커버리지 지표에 대한 이해
또 다른 커버리지 지표는 분기 커버리지(branch coverage)다. 분기 커버리지는 코드 커버리지의 단점을 극복하는 데 도움이 되므로 코드 커버리지보다 더 정확한 결과를 제공한다. 분기 커버리지 지표는 윈시 코드 라인 수를 사용하는 대신 if
문과 switch
문과 같은 제어 구조에 중점을 둔다.
분기 커버리지 = 통과 분기 / 전체 분기 수
이전의 예를 다시보자.
public static bool IsStringLong(string input) {
return input.Length > 5;
}
public void Test() {
bool result = IsStringLong("abc");
Assert.Equal(false, result);
}
IsStringLong
메서드에 두 개의 분기가 있는데, 하나의 문자열 인수의 길이가 다섯 자를 초과하는 상황에 대한 것이고 다른 하나는 그렇지 않은 경우다. 테스트는 이런 분기 중 하나에 대해서만 적용되므로 분기 커버지리 지표는 1/2 = 0.5 = 50%다.
🎈 커버리지 지표에 관한 문제점
분기 커버리지로 코드 커버리지보다 더 나은 결과를 얻을 수 있지만, 테스트 스위트 품질을 결정하는 데 어떤 커버리지 지표도 의존할 수 없는 이유는 다음과 같다.
- 테스트 대상 시스템의 모든 가능한 결과를 검증한다고 보장할 수 없다.
- 외부 라이브러리의 코드 경로를 고려할 수 있는 커버리지 지표는 없다.
단지 코드 경로를 통과하는 것이 아니라 실제로 테스트하려면, 단위 테스트에는 반드시 적절한 검증이 있어야 한다. 다시 말해, 테스트 대상 시스템이 낸 결과가 정확히 예상하는 결과인지 확인해야 한다. 커버리지 지표가 의미가 있으려면, 모든 측정 지표를 검증해야 한다.
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"); // true 반환
bool result2 = IsStringLong("abcdef"); // false 반환
}
이 테스트에서는 코드 커버리지와 분기 커버리지가 둘 다 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
메서드가 수행하는 코드 경로는 고려하지 않는다.
수많은 예외 상황에 빠질 수 있지만, 테스트에서 모든 예외 상황을 다루는지 확인할 방법이 없다. 이는 커버리지 지표가 외부 라이브러리의 코드 경로를 고려해야 한다는 것이 아니라, 해당 지표로는 단위 테스트가 얼마나 좋은지 나쁜지를 판단할 수 없다는 것을 보여준다. 커버리지 지표로 테스트가 철저한지 또는 테스트가 충분한지 알 수는 없다.