4.2 테스트 커버리지와 테스트 효과성
개요 및 동기
테스트 커버리지는 테스트 스위트에 의해 실행된 코드의 백분율을 측정한다: 라인 커버리지, 브랜치 커버리지, 혹은 더 엄격한 경로 커버리지. 그것은 이 책 전체에서 가장 널리 추적되는 메트릭 중 하나이며, 계산하기 저렴하고, 단일한 백분율로 시각화하기 쉬우며, 결과적으로 목표가 되는 어떤 메트릭에 대해서도 주제 1.2가 예측하는 바로 그 방식으로 가장 자주 게임당하는 것 중 하나다. 테스트 스위트는 거의 아무런 의미 있는 것도 검증하지 않으면서도 높은 커버리지를 달성할 수 있는데, 커버리지는 코드가 테스트 실행 중에 실행되었는지를 측정하지, 테스트가 실제로 그 코드가 올바르게 작동했는지 확인했는지를 측정하지 않기 때문이다.
커버리지와 진정한 테스트 효과성 사이의 이 간극은 사소한 각주가 아니다. 그것은 이 주제의 핵심 관심사다. 함수를 호출하지만 그 결과에 대해 아무것도 단언하지 않는 테스트는 경계 사례에 걸쳐 함수의 행동을 철저하게 검증하는 테스트와 동일하게 커버리지를 높인다. 이 주제가 권장하는 해결책인 뮤테이션 테스트는 코드에 작고 인위적인 결함을 의도적으로 도입하고 테스트 스위트가 실제로 그것들을 잡아내는지 확인하며, 이 간극에 대한 직접적인 답이고, 이 주제는 그것을 선택적인 추가 사항이 아니라 커버리지의 필수적인 보완물로 취급한다.
대규모 팀에게 커버리지 목표는 흔히 조직 전체의 품질 게이트로 채택되며, 이는 정확히 주제 1.2가 게임에 가장 노출된다고 경고하는 종류의 인센티브가 부여되고 눈에 잘 띄는 메트릭이다. 효과성 확인 없이 포괄적인 커버리지 백분율 요건을 설정하는 엔터프라이즈와 정부 조직은, 사실상, 이 책이 서술하는 정확히 그 임계값 게임 패턴을 장려하고 있는 것이다: 실제 결함 예방의 어떤 상응하는 개선도 없이 순전히 수치에 도달하기 위해 작성된 사소한 테스트.
핵심 원칙
- 커버리지는 검증이 아니라 실행을 측정한다. 테스트에 의해 실행되는 줄은 테스트가 그것에 대해 의미 있는 무언가를 확인했는지에 대해 아무것도 말해 주지 않는다.
- 효과성 확인 없는 커버리지 목표는 교과서적인 굿하트의 법칙 설정이다(주제 1.2): 수치는 개선되지만 진정한 품질은 그렇지 않다.
- 뮤테이션 테스트는 커버리지의 대체물이 아니라 필수적인 보완물이다. 둘 다 함께 사용하라.
- 커버리지는 최대화해야 할 목표보다 바닥으로서 더 유용하다. 낮은 수치는 진정으로 테스트되지 않은 코드를 드러낸다. 100%를 좇는 것은 흔히 체감하거나 부정적인 수익을 만들어 낸다.
- 중요 경로 커버리지가 균일하고 포괄적인 커버리지보다 더 중요하다. 모든 코드가 실패할 경우 동등한 위험을 지니는 것은 아니다.
권장 사항
최대화할 목표가 아니라 테스트되지 않은 코드를 찾기 위해 커버리지를 사용하라
커버리지 보고서를 100%를 향해 밀어붙여야 할 점수로서가 아니라 주로 전혀 테스트가 없는 것의 지도로 취급하라, 이는 진정으로 유용한 정보다. 커버리지가 0인 코드는 메울 가치가 있는 실제 간극이다. 커버리지를 85%에서 95%로 밀어붙이는 한계 가치는 보통 훨씬 더 낮으며, 특히 그 노력이 단지 더 높은 수치에 도달하기 위해 낮은 가치의 테스트를 만들어 낸다면 그만한 노력의 가치가 없는 경우가 흔하다.
모든 커버리지 목표를 뮤테이션 테스트와 짝지어라
뮤테이션 테스트 도구는 비교 연산자를 뒤집거나 경계 조건을 바꾸는 등 코드에 작은 결함을 자동으로 도입한 다음, 각 변형된 버전에 대해 당신의 테스트 스위트를 실행한다. 대부분의 뮤턴트를 “제거”(그것에 맞서 실패)하는 테스트 스위트는 진정으로 행동을 검증하고 있는 것이다. 높은 라인 커버리지를 가지지만 낮은 뮤턴트 제거율을 가진 테스트 스위트는 코드를 의미 있게 확인하지 않으면서 실행하고 있는 것이다. 이 짝짓기는 커버리지 목표 게임에 대한 단일한 가장 효과적인 가드레일이며, 이 책은 그것을 고급이거나 선택적인 기법이 아니라 표준 관행으로 권장한다.
중요 경로에 먼저 커버리지와 뮤테이션 테스트의 우선순위를 두라
모든 코드가 동등한 위험을 지니는 것은 아니다. 결제 처리 경로, 인증 확인, 혹은 데이터 마이그레이션 스크립트는 드물게 사용되는 관리 보고서보다 훨씬 더 엄격한 테스트를 받을 자격이 있다. 전체 코드베이스에 걸쳐 균일한 커버리지를 추구하는 대신, 가장 위험이 높고 가장 결과가 큰 코드 경로를 식별하고 커버리지와 뮤테이션 테스트 노력 둘 다를 먼저 그곳에 집중하며, 진정으로 위험이 낮은 코드에 대한 더 낮은 커버리지를 간과가 아니라 의도적이고 정보에 입각한 트레이드오프로 받아들여라.
구체적인 커버리지 게임 패턴을 경계하라
일단 목표가 되면 커버리지가 게임당하는 가장 흔한 방법은 다음을 포함한다: 함수를 호출하지만 결과에 대해 의미 있는 아무것도 단언하지 않는 테스트(이 메트릭에 적용된 주제 1.2의 임계값 게임), 근본적인 문제를 고치는 대신 실패하는 테스트를 비활성화하거나 삭제하는 것, 그리고 왜 테스트하기 어려운지 다루는 대신 테스트하기 어려운 코드를 커버리지 계산에서 완전히 제외하는 것. 커버리지 백분율만 신뢰하는 대신 테스트의 표본을 직접 주기적으로 감사하여 그 실제 단언을 읽어 보라.
CI 파이프라인에서 커버리지 천장이 아니라 커버리지 바닥을 설정하라
모든 변경이 전체 수치를 더 높이도록 요구하는 대신, 새로운 코드에 대해 합의된 바닥 아래로 커버리지가 떨어지면 빌드가 실패하도록 구성해 퇴보를 막아라. 이 구분이 중요하다: 바닥은 수치를 더 밀어 올리기 위해 순전히 작성되는 낮은 가치의 테스트를 만들어 내는 동일한 끊임없는 상향 압박을 만들지 않으면서 후퇴에 맞서 보호한다.
트레이드오프: 장단점
| 접근법 | 장점 | 단점 |
|---|---|---|
| 커버리지 백분율만 | 저렴하고, 단순하며, 도구에 의해 널리 지원됨 | 쉽게 게임당함; 검증이 아니라 실행을 측정함 |
| 뮤테이션 테스트와 함께하는 커버리지 | 테스트가 실제로 행동을 확인하는지 검증하며, 게임에 저항함 | 계산적으로 더 비쌈; 도구 투자가 필요함 |
| 코드베이스 전반의 균일한 커버리지 목표 | 명시하고 시행하기 단순함 | 낮은 위험 코드에 노력을 낭비함; 다른 곳의 진정한 위험에 과소 투자함 |
| 위험 기반, 중요 경로 우선 커버리지 | 가장 중요한 곳에 노력을 집중시킴 | 진정으로 중요한 경로를 올바르게 식별하려면 판단력이 필요함 |
핵심 긴장은 단순성 대 정직함이다. 단일한 커버리지 백분율은 보고하기 쉽고 목표로 설정하기 쉽지만, 그 단순성이야말로 그것이 인센티브가 부여된 수치가 되는 순간 그토록 쉽게 게임당하게 만드는 것이다. 정직한 신호의 비용으로서 뮤테이션 테스트와 위험 기반 우선순위 지정의 추가된 복잡성을 받아들이고, 중요 경로에 올바르게 집중되고 강한 뮤턴트 제거율로 뒷받침되는 더 낮은 전체 커버리지 수치가 더 높지만 더 균일하게 분산되고 덜 효과적으로 검증된 수치보다 더 가치 있는 이유를 당신의 팀에게 명시적으로 전달함으로써 이 긴장을 해결하라.
팀과 논의할 질문
우리의 가장 위험이 높은 코드 경로에서 우리의 뮤턴트 제거율은 무엇이며, 그것은 같은 코드의 커버리지 백분율과 어떻게 비교되는가? 높은 커버리지 수치와 낮은 뮤턴트 제거율 사이의 큰 간극은 커버리지 홀로는 당신이 생각하는 것을 말해 주지 않는다는 가장 명확하게 가능한 신호다.
무엇을 검증해야 할지에 대한 실질적인 고민 거의 없이 주로 커버리지 수치를 높이기 위해 테스트를 작성한 적이 있는가? 여기서 정직해져라. 이것은 팀이 인정하고 싶어 하는 것보다 더 자주 일어나며, 특히 커버리지 게이트가 병합을 막고 있는 마감 압박 아래서 그렇다.
우리의 커버리지 노력은 우리의 가장 위험이 높은 코드 경로에 집중되어 있는가, 아니면 그 코드가 실패할 경우의 결과와 무관하게 균일하게 퍼져 있는가? 당신의 현재 커버리지 분포를 당신의 코드베이스에 대한 정직한 위험 평가와 비교하고 불일치를 찾아보라.
그것이 드러낸 근본적인 문제를 고치는 대신 실패하는 테스트를 비활성화하거나 삭제한 적이 있는가? 이것은 커버리지 게임의 가장 해로운 형태 중 하나인데, 보고된 커버리지 수치는 거의 움직이지 않을 수 있는 동안 실제 보호를 적극적으로 제거하기 때문이다.
우리의 CI 파이프라인은 새로운 코드에 대한 커버리지 바닥을 시행하는가, 아니면 체감하는 수익과 무관하게 점점 더 높은 천장을 밀어붙이는가? 당신의 현재 게이트 설계가 올바른 인센티브, 즉 퇴보에 맞선 보호를 만드는지, 아니면 잘못된 인센티브, 즉 낮은 가치의 테스트 패딩을 보상하는 끊임없는 상향 압박을 만드는지 논의하라.
우리 코드베이스에서 어떤 코드가 커버리지 계산에서 제외되며, 그 제외는 정당화되는가 아니면 실제 테스트 간극을 숨기고 있는가? 당신의 실제 제외 설정을 검토하라. 아무도 각 제외가 여전히 정당화되는지 다시 살펴보지 않은 채 이 목록이 시간이 지남에 따라 조용히 자라는 것은 흔하다.
분야별 관점
스타트업. 공식적인 커버리지 목표는 이렇게 이른 시기에는 흔히 불필요하다. 여전히 빠르게 변화하고 있고 어쨌든 곧 상당히 재작성될 수 있는 코드베이스에 걸쳐 포괄적인 백분율을 추구하는 대신 당신의 가장 위험하고 가장 비즈니스에 중요한 코드 경로(보통 결제나 핵심 워크플로 논리)에 테스트 작성 노력을 직접 집중하라.
중소기업. 대부분의 CI 플랫폼은 최소한의 설정 비용으로 자동으로 커버리지를 보고한다. 특정 목표 백분율을 좇는 대신 주로 완전히 테스트되지 않은 중요한 코드를 발견하는 데 그것을 사용하고, 그것이 드러내는 것에 행동할 엔지니어링 역량을 갖춘 후에만 뮤테이션 테스트를 고려하라.
엔터프라이즈. 포괄적이고 조직 전체의 커버리지 목표는 이 규모에서 흔하고 중대한 실수인데, 그것들은 수십 개 팀에 걸쳐 동시에 정확히 이 주제가 서술하는 게임을 장려하기 때문이다. 서비스 중요도에 따라 달라지는 위험 기반 커버리지 기대치를 확립하고, 특별히 가장 위험이 높은 시스템을 위해 뮤테이션 테스트 인프라에 투자하라.
정부. 커버리지 요건은 때때로 조달이나 준수 문서에 품질 보증을 위한 무딘, 쉽게 명시할 수 있는 대리 지표로 나타난다. 가능한 곳에서는, 계약상의 인센티브가 이 주제가 경고하는 낮은 가치의 테스트 패딩을 뜻하지 않게 보상하지 않도록, 계약상 요구되는 어떤 커버리지 백분율이든 뮤테이션 테스트나 결함 기반 효과성 요건과 짝지어라.
사례
엔터프라이즈. 한 이커머스 플랫폼의 리더십은 모든 새로운 코드에 대해 회사 전체의 95% 커버리지 요건을 설정했으며, 엄격한 CI 게이트로 시행했다. 겉보기에는 잘 테스트된 코드에서의 프로덕션 결함 물결에 촉발된 2년 후의 감사는 코드베이스의 상당 부분에 걸쳐 40% 미만의 뮤턴트 제거율을 발견했다: 팀들은 마감 압박 아래서 순전히 게이트를 만족시키기 위해 그 행동에 대해 의미 있게 단언하지 않으면서 코드 경로를 실행하는 테스트를 작성해 왔다. 그 회사는 포괄적인 커버리지 요건을 위험 등급별 정책으로 대체했다: 결제와 인증 코드에 대해서는 엄격한 커버리지에 더해 80% 제거율 임계값 이상의 의무적인 뮤테이션 테스트, 그리고 낮은 위험의 내부 도구에 대해서는 훨씬 더 가벼운 커버리지 바닥. 이는 낭비된 테스트 노력을 줄이면서 진정으로 중요한 경로의 결함률을 측정 가능하게 개선했다.
정부. 한 공공 보건 기관의 복지 자격 시스템은 그 개발 공급업체 계약 아래서 90% 테스트 커버리지를 유지하도록 계약상 요구되어 왔다. 커버리지 요건이 충족되었음에도 출시된 중대한 자격 계산 결함을 따른 사후 검토는 책임이 있는 특정 함수가 전적으로 유효한 입력으로 함수를 호출하지만 결함이 정확히 발생한 지점인 경계 조건이나 무효한 입력은 결코 테스트하지 않는 테스트를 통해 그 커버리지를 달성했다는 것을 발견했다. 그 기관의 수정된 공급업체 계약은 이제 어떤 자격 계산 코드에 대해서도 커버리지와 함께 문서화된 뮤테이션 테스트 점수를 요구하며, 이는 준수하지만 비효과적인 테스트가 계약을 만족시킬 수 있게 했던 특정 간극을 메운다.
비즈니스 사례: 동기, ROI, TCO
커버리지를 뮤테이션 테스트와 짝짓는 것의 수익은 프로덕션 결함의 비용을 치르기 전에 겉보기 테스트 품질과 실제 테스트 품질 사이의 간극을 잡아내는 것이다. 위의 이커머스 사례는 그 패턴을 명확하게 보여 준다: 커버리지 요건 홀로는 거짓된 안전감을 만들어 냈고, 이는 결국 그 간극을 더 일찍 잡아냈을 뮤테이션 테스트 투자보다 훨씬 더 큰 비용으로 결함의 물결에 의해 드러났다.
총 소유 비용은 단순한 커버리지 계측보다 실행하는 데 더 비싸서 보통 전체 코드베이스가 아니라 중요 경로 코드를 위해 남겨지는 뮤테이션 테스트의 계산 비용과, 결과를 해석하고 행동하는 엔지니어링 시간을 포함한다. 그 비용은 특별히 테스트 효과성의 발견되지 않은 간극의 비용이 가장 높은 가장 위험이 높은 코드에 대해 정당화된다.
안티패턴과 함정
- 커버리지 백분율을 직접적인 품질 평결로 취급하기: 그것은 검증이 아니라 실행을 측정한다.
- 주로 커버리지 게이트를 만족시키기 위해 테스트 작성하기: 정확히 주제 1.2가 경고하는 낮은 가치의 임계값 게임 패턴을 만들어 낸다.
- 근본적인 문제를 고치는 대신 실패하는 테스트를 비활성화하거나 삭제하기: 보고된 수치에는 거의 영향을 미치지 않으면서 실제 보호를 제거한다.
- 코드 위험과 무관하게 균일한 커버리지 목표 적용하기: 낮은 위험 코드에 노력을 낭비하고 진정으로 중요한 경로에 과소 투자한다.
- 시간이 지남에 따라 조용히 제외 목록을 키우기: 기술적으로는 정확하지만 오도하는 커버리지 수치 뒤에 실제 테스트 간극을 숨긴다.
- 커버리지 바닥이 아니라 커버리지 천장을 추구하기: 진정한 검증보다 테스트 패딩을 보상하는 끊임없는 상향 압박을 만든다.
성숙도 모델
- 1단계, 시작: 커버리지가 측정되지 않거나, 바닥, 목표, 혹은 효과성 확인 없이 일관성 없이 측정된다.
- 2단계, 개발: 커버리지 목표가 존재하고 추적되지만, 뮤테이션 테스트나 위험 기반 우선순위 지정이 노력이 어떻게 배분되는지 알려주지 않는다.
- 3단계, 표준화: 커버리지 바닥이 CI에서 일관되게 시행되며, 위험 기반 우선순위 지정이 커버리지 노력이 집중되는 곳을 이끈다.
- 4단계, 관리: 뮤테이션 테스트가 중요 경로 코드에서 실행되며, 커버리지와 함께 충족되어야 하는 추적된 제거율 임계값이 있고, 제외 목록이 주기적으로 감사된다.
- 5단계, 조율: 조직은 뮤테이션 테스트에 정보를 받은 우선순위 지정으로 거슬러 올라가는 구체적인 결함 감소를 제시할 수 있으며, 커버리지와 효과성 데이터가 함께 테스트 투자 결정을 직접 알려준다.
논의를 위한 아이디어
- 우리의 단 하나의 가장 중요한 코드 경로에서 우리의 뮤턴트 제거율은 무엇이며, 우리는 그것을 알고 있기나 한가?
- 순전히 커버리지 게이트를 만족시키기 위해 낮은 가치의 테스트를 작성한 적이 있는가?
- 우리의 현재 커버리지 노력은 위험이 가장 높은 곳에 집중되어 있는가, 아니면 균일하게 퍼져 있는가?
- 현재 커버리지 계산에서 제외되는 코드는 무엇이며, 그 제외는 여전히 정당화되는가?
- 우리의 가장 위험이 높은 시스템에 대한 뮤테이션 테스트 투자는 그 계산 비용의 가치가 있을까?
핵심 요약
- 테스트 커버리지는 검증이 아니라 실행을 측정한다. 커버된 줄은 그것이 의미 있게 확인되었는지에 대해 아무것도 말해 주지 않는다.
- 테스트가 코드를 실행한다는 것뿐 아니라 실제로 실제 결함을 잡아낸다는 것을 검증하기 위해 커버리지를 뮤테이션 테스트와 짝지어라.
- 전체 코드베이스에 걸쳐 균일한 커버리지를 추구하는 대신 중요하고 위험이 높은 경로에 테스트 노력을 집중하라.
- 커버리지를 끊임없이 최대화할 천장이 아니라 퇴보에 맞서 보호할 바닥으로 사용하라.
- 구체적인 커버리지 게임 패턴을 경계하라: 낮은 가치의 테스트, 비활성화된 실패 테스트, 조용히 자라나는 제외 목록.
참고 문헌 및 추가 자료
- Feathers, Michael. Working Effectively with Legacy Code. Prentice Hall, 2004.
- Jia, Yue, and Mark Harman. “An Analysis and Survey of the Development of Mutation Testing.” IEEE Transactions on Software Engineering, 2011.
- Meszaros, Gerard. xUnit Test Patterns: Refactoring Test Code. Addison-Wesley, 2007.
- Forsgren, Nicole, Jez Humble, and Gene Kim. Accelerate: The Science of Lean Software and DevOps. IT Revolution Press, 2018.