Code Coverage를 넘어: 견고한 애플리케이션을 위한 Mutation Testing 실용 가이드

발행: (2025년 12월 7일 오후 06:36 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

우리는 모두 그 짧은 도파민 샷을 경험합니다.
코드를 푸시하고 CI/CD 파이프라인이 실행되면, 몇 분 뒤… Green Build가 됩니다. 모든 불빛이 초록색이고 대시보드에 Code Coverage가 90 % (가장 열심인 사람은 100 %까지)라고 자랑스럽게 표시됩니다.

종이 위에서는 소프트웨어 품질이 흠잡을 데 없이 보입니다. 두 눈을 감고 푹 잘 수 있죠.
그런데 이틀 뒤, 여러분이 완벽하다고 생각했던 기능에 치명적인 버그가 프로덕션에서 터집니다. 어떻게 가능한 걸까요? 자동화 테스트는 통과했잖아요?

여기서 불편한 진실을 마주해야 합니다: 높은 코드 커버리지는 애플리케이션이 정상 동작한다는 것을 절대 보장하지 않습니다. 코드를 실행했는지만 보장하고, 검증했는지는 보장하지 않죠.

정말 안심하고 싶고 단위 테스트의 신뢰성을 확보하고 싶다면, 커버리지만 믿는 것을 멈추고 다음 단계인 Mutation Testing으로 넘어가야 합니다.


I. Code Coverage : 유용하지만 위험한 지표

구체적으로 Code Coverage단위 테스트 실행 시 여러분의 소스 코드 중 몇 퍼센트가 실행되었는지를 측정합니다.
커버리지가 10 %라면 애플리케이션은 블랙박스이며, 매 배포가 위험한 도박이 됩니다. 죽은 코드나 놓친 논리 분기(예: 연 1회만 실행되는 else)를 찾아내는 훌륭한 도구이기도 합니다.

“Vanity Metric” 함정

이 지표를 절대 목표로 바꾸면 문제가 생깁니다.
개발자는 100 % 커버리지를 보여주면서도 실제로는 아무것도 테스트하지 않은 테스트 스위트를 쉽게 만들 수 있습니다.

어떻게? 결과를 검증하지 않고(어설션 없이) 코드를 실행하면 됩니다.

Code Coverage는 코드가 실행되었음을 보장하지만, 올바르게 동작했음을 보장하지는 않습니다.

보안 요원의 비유

집을 지키는 보안 요원을 고용했다고 생각해 보세요. 그의 임무는 모든 방을 확인하는 것입니다(커버리지).

  • 거실, 주방, 침실을 모두 방문하고…
  • 방을 100 % 방문했나요? 네.
  • 창문이 닫혔는지 확인했나요? 아니요.
  • 가스가 차단됐는지 확인했나요? 아니요.
  • 커튼 뒤에 침입자가 있는지 보았나요? 아니요.

그는 단지 “그곳을 지나갔을 뿐”입니다. 많은 자동화 테스트가 하는 일과 똑같습니다: 커버리지를 올리기 위해 함수들을 지나가지만, 비즈니스 규칙을 충분히 엄격히 검증하지는 않죠.

바로 여기서 더 편집증적인 보안 요원이 필요합니다: Mutation Testing.


II. Mutation Testing : 감시자를 감시한다

Code Coverage가 코드를 실행했는지를 확인한다면, Mutation Testing(변이 테스트)은 여러분의 테스트가 유용한지를 확인합니다.
철학이 완전히 다릅니다: 코드를 보는 대신 테스트 자체를 테스트합니다.

구체적인 동작 방식

전체 과정은 Mutation Testing 도구(예: Stryker, Infection, PIT)로 자동화됩니다. 파이프라인에서 일어나는 일은 다음과 같습니다:

  1. 도구가 현재 정상적인 소스 코드를 가져옵니다.
  2. 아주 작은 논리 규칙을 바꿔 “Mutant”(변이체) 를 만든 복사본을 생성합니다.
    예: +-로 바꿈.
    예: return truereturn false로 바꿈.
    예: 함수 호출을 삭제함.
  3. 변이된 코드를 가지고 여러분의 테스트 스위트를 실행합니다.

“Mutant Killed” vs “Mutant Survived”

테스트가 실패하기를 기대하는 순간입니다.

시나리오테스트 결과해석
1. 테스트가 통과 (초록 🟢)변이가 살아남음 (Mutant Survived)해당 코드 부분에 대한 테스트가 비효율적이거나 불완전합니다.
2. 테스트가 실패 (빨강 🔴)변이가 사살됨 (Mutant Killed)테스트가 변화를 감지했습니다: 견고합니다.

MSI : 신뢰할 수 있는 유일한 지표

분석이 끝나면 도구는 MSI (Mutation Score Indicator) 라는 점수를 제공합니다. 이는 여러분의 테스트가 사살한 변이체의 비율을 의미합니다.

Code Coverage가 100 %이고 MSI가 50 %라면, 코드 한 줄 중 절반은 프로덕션에 배포되기 전까지 아무도 눈치채지 못하고 깨질 수 있다는 뜻입니다. 이 지표는 “예쁘게 보이기 위한” 테스트와 장기적인 코드 품질을 보장하는 진정한 안전망을 구분해 줍니다.


III. 구체적인 예시 : 변이가 살아남을 때

경계 오류 (클래식)

사용자가 할인을 받을 자격이 있는지를 판단하는 간단한 함수를 생각해 보세요. 규칙은 “100 € 초과 금액을 사용했을 때만” 입니다.

function isEligibleForDiscount(amount) {
    if (amount > 100) {
        return true;
    }
    return false;
}

여러분의 단위 테스트 (Coverage 100 %)

  • isEligibleForDiscount(50)false (정상)
  • isEligibleForDiscount(150)true (정상)

테스트는 모두 통과하고 커버리지는 100 %입니다.

변이체 공격

Mutation Testing 도구가 비교 연산자를 바꿉니다: >>= 로 변경합니다.

// 생성된 변이체
function isEligibleForDiscount(amount) {
    if (amount >= 100) { // 여기서 변이가 발생!
        return true;
    }
    return false;
}

결과

원본 코드와 변이된 코드 모두에 대해 동일한 테스트(50150)를 실행하면 결과가 동일합니다:

  • 50false (여전히 100)

판단: Mutant Survived.
변화를 감지하지 못했습니다. 이 변이를 사살하려면 100이라는 경계값 테스트가 필요했습니다.


스택별 도구

선호하는 언어에 관계없이 성숙한 도구가 존재합니다:

  • JavaScript / TypeScript : StrykerJS
  • PHP (Symfony/Laravel) : Infection PHP
  • Java : PITest
  • C# / .NET : Stryker.NET

IV. 왜 투자해야 할까?

본문은 계속됩니다… (추가 전개 예정).

Back to Blog

관련 글

더 보기 »