변이 테스트의 원리와 변이 연산자를 이해하고, Stryker, PIT, mutmut 도구로 AI 생성 테스트의 품질을 검증하는 방법을 다룹니다. 변이 점수 측정과 비용-효과 분석도 포함합니다.
코드 커버리지 100%라는 수치가 테스트의 품질을 보장할까요? 안타깝게도 그렇지 않습니다. 코드를 실행하기만 하고 결과를 제대로 검증하지 않는 테스트는 커버리지는 높이지만, 실제 결함을 놓칩니다.
Mutation Testing(변이 테스트)은 이 문제를 해결합니다. 소스 코드에 의도적으로 작은 변경(변이)을 주입하고, 기존 테스트 스위트가 이 변이를 감지하는지 확인합니다. 테스트가 변이를 잡아내면 killed(사살), 잡아내지 못하면 survived(생존)으로 분류합니다.
Mutation Score(변이 점수)는 전체 변이체 중 사살된 변이체의 비율입니다.
변이 점수 = 사살된 변이체 / 전체 변이체 x 100%변이 점수가 높을수록 테스트 스위트의 결함 검출 능력이 뛰어납니다.
변이 테스트는 "테스트를 테스트하는 테스트"라고 할 수 있습니다. 코드 커버리지가 테스트의 범위를 측정한다면, 변이 점수는 테스트의 깊이를 측정합니다. 이 둘을 함께 사용할 때 테스트 품질을 종합적으로 평가할 수 있습니다.
변이 연산자는 소스 코드에 적용하는 변경 규칙입니다. 각 연산자는 개발자가 실수할 수 있는 실제 결함 패턴을 모방합니다.
| 범주 | 연산자 | 원본 | 변이 |
|---|---|---|---|
| 산술 | ArithmeticOperator | a + b | a - b |
| 관계 | RelationalOperator | a > b | a >= b |
| 논리 | LogicalOperator | a && b | a || b |
| 조건 | ConditionalExpression | if (x) | if (true) / if (false) |
| 단항 | UnaryOperator | -x | x |
| 문자열 | StringLiteral | "hello" | "" |
| 배열 | ArrayDeclaration | [a, b] | [] |
| 반환값 | ReturnValue | return x | return 0 / return null |
| 블록 | BlockStatement | 코드 블록 | 빈 블록 |
| 동등성 | EqualityOperator | === | !== |
다음 함수에 변이 연산자를 적용해 보겠습니다.
function calculateFinalPrice(
basePrice: number,
quantity: number,
discountRate: number
): number {
if (quantity <= 0) {
throw new Error("수량은 1 이상이어야 합니다");
}
let total = basePrice * quantity;
if (quantity >= 10) {
total *= 0.95; // 대량 구매 할인 5%
}
total *= 1 - discountRate;
return Math.round(total);
}이 함수에서 생성되는 변이체 예시는 다음과 같습니다.
// 변이체 1: 관계 연산자 변경 (quantity <= 0 -> quantity < 0)
if (quantity < 0) { ... }
// 변이체 2: 산술 연산자 변경 (basePrice * quantity -> basePrice + quantity)
let total = basePrice + quantity;
// 변이체 3: 관계 연산자 변경 (quantity >= 10 -> quantity > 10)
if (quantity > 10) { ... }
// 변이체 4: 상수 변경 (0.95 -> 1.05)
total *= 1.05;
// 변이체 5: 산술 연산자 변경 (1 - discountRate -> 1 + discountRate)
total *= 1 + discountRate;
// 변이체 6: 블록 제거 (대량 구매 할인 블록 전체 제거)
// if (quantity >= 10) { ... } -> 삭제
// 변이체 7: 반환값 변경 (Math.round(total) -> 0)
return 0;Stryker는 JavaScript/TypeScript 생태계에서 가장 널리 사용되는 변이 테스트 도구입니다.
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner @stryker-mutator/typescript-checker
npx stryker init{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.spec.ts"],
"testRunner": "jest",
"checkers": ["typescript"],
"reporters": ["html", "clear-text", "progress"],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
},
"concurrency": 4,
"timeoutMS": 10000
}npx stryker runStryker 실행 결과는 다음과 같은 형태로 출력됩니다.
----------|---------|----------|-----------|
File | % score | # killed | # survived|
----------|---------|----------|-----------|
cart.ts | 85.71 | 12 | 2 |
price.ts | 77.78 | 7 | 2 |
user.ts | 90.00 | 9 | 1 |
----------|---------|----------|-----------|
All files | 84.85 | 28 | 5 |Stryker의 thresholds 설정에서 break 값은 변이 점수가 이 값 미만으로 떨어지면 빌드를 실패시키는 기준입니다. CI 파이프라인에서 테스트 품질의 최소 기준을 강제할 수 있습니다.
PIT(PITest)는 Java 생태계의 표준 변이 테스트 도구입니다.
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.16.0</version>
<configuration>
<targetClasses>
<param>com.example.service.*</param>
</targetClasses>
<targetTests>
<param>com.example.service.*Test</param>
</targetTests>
<mutators>
<mutator>DEFAULTS</mutator>
<mutator>REMOVE_CONDITIONALS</mutator>
</mutators>
<mutationThreshold>70</mutationThreshold>
<outputFormats>
<outputFormat>HTML</outputFormat>
<outputFormat>XML</outputFormat>
</outputFormats>
</configuration>
</plugin>mvn org.pitest:pitest-maven:mutationCoveragemutmut은 Python 생태계의 변이 테스트 도구입니다.
pip install mutmut
mutmut run --paths-to-mutate=src/mutmut results
mutmut show 5 # 5번 생존 변이체의 상세 내용 확인2장에서 AI가 생성한 테스트가 실제로 결함을 잡아내는지 변이 테스트로 검증해 보겠습니다.
| 측정 항목 | AI 생성 테스트 | 수동 작성 테스트 |
|---|---|---|
| 테스트 케이스 수 | 15개 | 8개 |
| 라인 커버리지 | 92% | 85% |
| 분기 커버리지 | 88% | 80% |
| 변이 점수 | 75% | 82% |
| 생존 변이체 | 8개 | 5개 |
| 작성 시간 | 30초 | 2시간 |
흥미로운 점은 AI 생성 테스트가 커버리지는 높지만, 변이 점수는 수동 테스트보다 낮을 수 있다는 것입니다. 이는 AI가 코드를 실행하는 데는 탁월하지만, 결과를 검증하는 단언의 정밀도가 부족할 수 있기 때문입니다.
생존한 변이체를 분석하면 테스트의 약점을 파악할 수 있습니다.
// 원본: total *= 0.95
// 변이: total *= 1.05
// AI 생성 테스트에서 이 변이가 생존한 이유:
// 테스트가 대량 구매 시 할인이 적용되는지만 확인하고,
// 정확한 할인 금액을 검증하지 않았기 때문
test("대량 구매 시 할인이 적용된다", () => {
const result = calculateFinalPrice(1000, 10, 0);
expect(result).toBeLessThan(10000); // 느슨한 단언 -- 변이 감지 실패
});
// 개선된 테스트:
test("대량 구매 시 5% 할인이 적용된다", () => {
const result = calculateFinalPrice(1000, 10, 0);
expect(result).toBe(9500); // 정밀한 단언 -- 변이 감지 성공
});AI 생성 테스트의 가장 흔한 약점은 느슨한 단언(loose assertion)입니다. toBeTruthy(), toBeDefined(), toBeGreaterThan(0) 같은 단언은 커버리지는 올리지만, 미묘한 결함을 놓칩니다. 변이 테스트는 이러한 약점을 정확히 드러냅니다.
변이 테스트는 강력하지만 비용이 큽니다. 모든 변이체에 대해 전체 테스트 스위트를 실행해야 하므로, 대규모 프로젝트에서는 실행 시간이 수 시간에 달할 수 있습니다.
| 전략 | 설명 | 효과 |
|---|---|---|
| 증분 변이 | 변경된 코드에만 변이 적용 | 실행 시간 70-90% 감소 |
| 변이 샘플링 | 전체 변이의 일부만 무작위 선택 | 실행 시간에 비례하여 감소 |
| 빠른 실패 | 하나의 테스트가 변이를 감지하면 나머지 건너뜀 | 평균 50% 감소 |
| 병렬 실행 | 변이체별 테스트를 병렬로 실행 | 코어 수에 비례하여 감소 |
| 정적 분석 연계 | 도달 불가능한 변이를 사전 제거 | 불필요한 변이 10-20% 제거 |
name: Mutation Testing
on:
pull_request:
branches: [main]
jobs:
mutation-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed
run: |
echo "files=$(git diff --name-only origin/main...HEAD -- 'src/**/*.ts' | tr '\n' ',')" >> $GITHUB_OUTPUT
- name: Run incremental mutation test
if: steps.changed.outputs.files != ''
run: |
npx stryker run --mutate="${{ steps.changed.outputs.files }}"
- name: Check mutation score
run: |
SCORE=$(cat reports/mutation/mutation.json | jq '.schemaVersion' -r)
echo "Mutation score: $SCORE%"모든 PR에 전체 변이 테스트를 실행하는 것은 비현실적입니다. 권장 전략은 다음과 같습니다. (1) PR 단위에서는 변경된 파일에 대해서만 증분 변이 테스트를 실행합니다. (2) 주간 또는 릴리스 전에 전체 변이 테스트를 실행합니다. (3) 변이 점수가 기준 이하로 떨어지면 빌드를 실패시킵니다.
이 장에서는 변이 테스트를 통해 AI 생성 테스트의 실질적 품질을 검증하는 방법을 살펴보았습니다.
핵심 내용을 정리하면 다음과 같습니다.
6장에서는 시각적 회귀 테스트를 다룹니다. 픽셀 단위 비교의 한계를 넘어, Applitools Eyes의 Visual AI가 어떻게 강화 학습을 활용하여 의미 있는 시각적 변화만 감지하는지, 그리고 Percy, Chromatic 등과의 차이점을 분석합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
픽셀 비교의 한계를 넘어 Visual AI 기반 시각적 회귀 테스트를 다룹니다. Applitools Eyes, Percy, Chromatic 비교 분석과 동적 콘텐츠 처리, 반응형 레이아웃 테스트, 스토리북 통합을 안내합니다.
자연어를 E2E 테스트로 변환하는 Momentic, testRigor, Functionize와 DOM 변경에 자동 적응하는 셀프 힐링 기능, Playwright와 AI를 결합한 실전 E2E 테스트 자동화를 다룹니다.
테스트 로트(Test Rot) 문제의 근본 원인과 AI 기반 셀프 힐링, 셀렉터 자동 재바인딩, 테스트 코드 리팩터링, 중복 테스트 감지, 커버리지 갭 분석 등 유지보수 비용 절감 전략을 다룹니다.