뮤테이션 테스팅

DoTheTest·2025년 8월 18일
0

테스트 지식

목록 보기
23/24

우리는 코드의 품질을 높이기 위해 열심히 단위 테스트를 작성하고, 코드 커버리지 리포트를 보며 안도감을 느낍니다. "코드 커버리지 90% 달성! 이제 우리 코드는 안전하겠지?"

하지만 정말 그럴까요? 여기 코드 커버리지 100%를 달성하는 테스트가 있습니다.

// Calculator.java
public int add(int a, int b) {
    return a + b;
}

// CalculatorTest.java
@Test
void testAdd() {
    Calculator calculator = new Calculator();
    calculator.add(1, 1); // 실행은 했지만...
    assertTrue(true);    // 결과 검증은 하지 않는다!
}

이 테스트는 add 메서드의 모든 라인을 실행하므로 코드 커버리지는 100%입니다. 하지만 만약 개발자가 return a - b;로 잘못 코딩했더라도, 이 테스트는 여전히 성공할 것입니다. 즉, 버그를 전혀 잡지 못하는 '무의미한 테스트'인 셈입니다.

이처럼 코드 커버리지는 테스트의 '양'을 측정할 뿐, '질'을 보장하지 못합니다. 그렇다면, "내 테스트가 정말로 버그를 잘 잡는지" 어떻게 증명할 수 있을까요? 그 해답이 바로 뮤테이션 테스팅(Mutation Testing)에 있습니다.

1. 뮤테이션 테스팅이란 무엇인가?

뮤테이션 테스팅이란, "당신의 테스트 코드가 얼마나 훌륭한지"를 테스트하는 방법입니다.

조금 더 구체적으로는, 운영 코드에 의도적으로 작은 버그(Mutation, 돌연변이)를 주입한 뒤, 기존의 테스트 스위트가 이 버그를 잡아내는지(테스트가 실패하는지) 확인하는 기법입니다.

마치 백신을 개발하는 과정과 같습니다. 약화된 바이러스(돌연변이)를 우리 몸(테스트 스위트)에 주입했을 때, 면역 체계(테스트 케이스)가 이를 감지하고 항체를 만들어내는지를 확인하는 것입니다.

2. 뮤테이션 테스팅의 핵심 용어

  • 돌연변이(Mutation): 코드에 가해지는 작은 변경. (예: >>=로, +-로 바꾸기, if문 조건 뒤집기 등)
  • 돌연변이 연산자(Mutation Operator): 어떤 종류의 돌연변이를 만들 것인지 정의하는 규칙.
  • 돌연변이체(Mutant): 돌연변이가 적용된 코드 버전.
  • 죽이기(Kill): 테스트가 돌연변이체를 감지하여 실패하는 것. (목표 달성 - 좋은 상태)
  • 생존(Survive): 테스트가 돌연변이체를 감지하지 못하고 여전히 성공하는 것. (문제 발생 - 테스트 보강 필요)
  • 뮤테이션 점수(Mutation Score): (죽인 돌연변이체 수 / 전체 돌연변이체 수) * 100. 이 점수가 높을수록 테스트 스위트의 품질이 높음을 의미합니다.

3. 뮤테이션 테스팅 실전 예제 (feat. Pitest)

가장 널리 쓰이는 Java용 뮤테이션 테스팅 도구인 Pitest를 예로 들어보겠습니다.

Step 1: 테스트할 코드와 불완전한 테스트

// AgeValidator.java
public class AgeValidator {
    public boolean isAdult(int age) {
        return age >= 19;
    }
}

// AgeValidatorTest.java
@Test
void 스무살은_성인이다() {
    AgeValidator validator = new AgeValidator();
    assertTrue(validator.isAdult(20));
}

이 테스트는 isAdult 메서드를 실행하므로 구문/분기 커버리지는 100%입니다.

Step 2: 뮤테이션 테스팅 실행

Pitest를 실행하면, 다음과 같은 돌연변이체들을 자동으로 생성하고 테스트를 다시 실행합니다.

  • Mutant 1: return age > 19; (>=>로 변경)
  • Mutant 2: return age <= 19; (>=<=로 변경)
  • Mutant 3: return false; (무조건 false 반환)
  • ... 등등

Step 3: 결과 분석

Pitest 리포트를 확인하면 다음과 같은 결과를 볼 수 있습니다.

  • Mutant 2, 3: KILLED. isAdult(20) 테스트가 이 돌연변이 코드에서는 실패하기 때문입니다.
  • Mutant 1: SURVIVED. isAdult(20) 테스트는 20 > 19도 참이므로, 이 돌연변이 코드에서도 여전히 성공합니다. 즉, 우리의 테스트는 >=>의 차이를 구분하지 못하는 '약한 테스트'였던 것입니다.

Step 4: 테스트 보강

생존한 돌연변이를 죽이기 위해, 우리는 경계값 테스트를 추가해야 합니다.

// AgeValidatorTest.java
@Test
void 열아홉살은_성인이다() {
    AgeValidator validator = new AgeValidator();
    assertTrue(validator.isAdult(19));
}

이 테스트 케이스를 추가하고 다시 Pitest를 실행하면, Mutant 1(return age > 19;)은 isAdult(19) 호출 시 false를 반환하여 테스트에 실패하게 됩니다. 드디어 Mutant 1도 KILLED되고, 우리의 뮤테이션 점수는 올라갑니다.

4. 뮤테이션 테스팅의 장점과 현실적인 과제

  • 장점:

    • 테스트 스위트의 실제 결함 탐지 능력을 객관적으로 측정합니다.
    • 놓치고 있던 엣지 케이스나 경계값 테스트를 추가하도록 강력하게 유도합니다.
    • 테스트 코드의 품질(특히 Assertion 부분)에 대해 팀이 더 깊이 고민하게 만듭니다.
  • 과제:

    • 실행 속도: 수많은 돌연변이체를 생성하고 각각 테스트를 실행하므로, 일반적인 단위 테스트보다 훨씬 느립니다. 따라서 CI 파이프라인에서는 변경된 코드에 대해서만 실행하거나, 야간 배치로 실행하는 전략이 필요합니다.
    • 등가 돌연변이(Equivalent Mutants): 코드의 동작을 실질적으로 바꾸지 않는 돌연변이(예: i++++i로)는 영원히 죽일 수 없습니다. 이런 것들은 사람이 직접 분석하여 제외해야 할 수 있습니다.

5. 결론: 코드 커버리지를 넘어, 뮤테이션 점수로

뮤테이션 테스팅은 테스트의 '양'을 측정하는 코드 커버리지를 넘어, 테스트의 '질'을 측정하는 궁극의 기술입니다. 단순히 코드를 '실행'하는 것을 넘어, 우리의 테스트가 정말로 '결함을 잘 잡아내는지'를 증명해 줍니다.

매일 CI 파이프라인에서 실행하기는 부담스러울 수 있지만, 주기적으로 뮤테이션 점수를 측정하고 관리하는 것은 우리 팀의 테스트 스위트가 시간이 지나도 녹슬지 않고 계속해서 날카롭게 유지되도록 돕는 가장 강력한 건강 진단 도구가 될 것입니다.


#소프트웨어테스팅 #뮤테이션테스팅 #단위테스트 #테스트품질 #코드커버리지 #Pitest #Stryker

0개의 댓글