뮤테이션 테스트 — PIT로 테스트의 품질을 검증하기

커버리지는 코드 실행 여부만 셀 뿐 단언의 품질은 보지 못합니다. 뮤테이션 테스트는 코드를 일부러 변형해 테스트가 그 변형을 잡아내는지 확인합니다. PIT(pitest)의 원리와 설정, 뮤테이션 점수 해석을 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 커버리지가 “코드가 실행됐는가”만 셀 뿐 “결과를 검증했는가”는 보지 못한다는 한계를 짚었습니다. 단언 하나 없이도 커버리지 100%를 만들 수 있다는 게 그 약점이었죠. 뮤테이션 테스트(mutation testing)는 바로 이 빈틈을 정면으로 공격합니다. 코드를 일부러 조금씩 망가뜨려 보고, 테스트가 그 망가짐을 잡아내는지를 확인합니다. 자바에서 대표적인 도구가 PIT(pitest)입니다.

변이를 만들고 죽이기

뮤테이션 테스트의 원리 — 코드를 변형해 테스트가 잡는지 본다

핵심 아이디어는 단순합니다. 원본 코드의 한 부분을 작게 바꾼 변이체(mutant)를 만듭니다. 예를 들어 >=>로 바꾸거나, +-로, return xreturn null로 바꿉니다. 그런 다음 변이체에 대해 기존 테스트를 돌립니다.

  • 테스트가 실패하면, 변이를 잡아낸 것이므로 변이체가 “처치됐다(killed)“고 합니다. 좋은 신호입니다.
  • 테스트가 통과하면, 코드를 망가뜨렸는데도 아무도 눈치채지 못한 것이므로 변이체가 “생존했다(survived)“고 합니다. 테스트에 구멍이 있다는 뜻입니다.

뮤테이션 점수는 전체 변이체 중 처치된 비율입니다. 이 점수가 높다는 것은 “코드를 망가뜨리면 테스트가 알아챈다”, 즉 테스트가 실제로 동작을 검증하고 있다는 강력한 증거입니다.

커버리지가 못 보는 것을 본다

커버리지 vs 뮤테이션 점수

지난 글의 가짜 테스트를 다시 봅시다. 단언 없이 메서드만 호출하는 테스트는 커버리지 100%를 만들지만, 코드의 >=>로 바꿔도 호출 자체는 성공하므로 테스트가 통과합니다. 변이체가 생존하는 것이죠. 뮤테이션 점수는 이 테스트가 사실상 아무것도 검증하지 않음을 즉시 드러냅니다. 반대로 결과를 제대로 단언하는 테스트라면 변이체가 죽습니다. 이렇게 뮤테이션 테스트는 단언의 존재와 정확성을 직접 측정합니다.

PIT 설정과 실행

Gradle 플러그인을 적용합니다.

plugins {
    id 'info.solidsoft.pitest' version '1.15.0'
}

pitest {
    targetClasses = ['com.paldyn.shop.domain.*']
    mutators = ['STRONGER']
    threads = 4
    timestampedReports = false
}

실행하면 PIT가 대상 클래스의 변이체들을 만들고, 각 변이체마다 관련 테스트를 돌려 처치 여부를 집계합니다.

./gradlew pitest

리포트는 HTML로 생성되며, 소스 코드 위에 어떤 변이가 처치됐고(초록) 어떤 변이가 생존했는지(빨강) 라인 단위로 색칠해 보여 줍니다. 생존한 빨간 라인이 바로 테스트를 보강해야 할 지점입니다.

대표적인 뮤테이터

PIT는 여러 종류의 변이를 적용합니다. 몇 가지 흔한 예입니다.

조건 경계      >=  →  >        (Conditionals Boundary)
부정          ==  →  !=       (Negate Conditionals)
산술          +   →  -        (Math)
반환값        return x → null  (Return Values)
메서드 호출 제거  foo();  →  삭제   (Void Method Call)

STRONGERALL 그룹을 쓰면 더 많은 변이를 적용해 더 엄격하게 검사하지만, 그만큼 실행 시간이 길어집니다.

비용과 현실적인 운영

뮤테이션 테스트의 가장 큰 단점은 비용입니다. 변이체 하나마다 테스트를 돌리므로, 변이체가 수천 개면 테스트 스위트를 수천 번 실행하는 셈입니다. 전체 코드베이스에 매번 돌리는 것은 비현실적입니다.

그래서 실무에서는 범위를 좁혀 운영합니다. 핵심 도메인 로직처럼 결함 비용이 큰 모듈에만 적용하고, PIT의 --changeset(변경분만 분석) 기능으로 이번 커밋이 건드린 코드만 검사하거나, 야간 CI에서 별도로 돌립니다. 모든 코드의 뮤테이션 점수를 100%로 만드는 것이 목표가 아니라, 가장 중요한 로직의 테스트가 진짜 검증하고 있는지 확인하는 것이 목적입니다.

정리

뮤테이션 테스트는 코드를 일부러 변형한 변이체를 만들고, 테스트가 그 변형을 잡아 처치하는지로 테스트의 검증 능력을 측정합니다. 커버리지가 “실행했는가”를 본다면 뮤테이션 점수는 “틀리면 잡는가”를 보기에, 단언 없는 가짜 테스트를 정확히 드러냅니다. PIT로 손쉽게 적용할 수 있지만 실행 비용이 크므로, 핵심 도메인이나 변경분에 한정해 운영하는 것이 현실적입니다. 이로써 테스트 도구를 다룬 묶음을 마치고, 다음 글부터는 운영의 또 다른 축인 로깅으로 넘어갑니다.


지난 글: 테스트 커버리지 — JaCoCo로 측정하고 해석하기

다음 글: 자바 로깅 개요 — 파편화된 생태계 이해하기


읽어주셔서 감사합니다. 😊