You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
테스트가 실제로 버그를 막는지 mutation(원본 코드를 일부러 망가뜨리는 변형)을 심어 검증하고, 결과를 커밋 메시지 trailer로 남긴다. 테스트의 단언이나 케이스를 새로 쓰거나 고쳤을 때, 커밋 게이트가 Mutation-Verified 기록을 요구했을 때, "이 테스트 믿어도 되나" "테스트가 제대로 검증하는지 봐줘" "false green 아닌지 확인해줘" 같은 요청이 있을 때 반드시 사용할 것. 커버리지를 올려달라거나 커버리지로 테스트 품질을 확인해달라는 요청도 이 스킬로 처리한다 — 커버리지는 코드가 실행됐다는 것만 말할 뿐 버그 방어력을 재지 않기 때문. 테스트를 새로 작성한 직후라면 요청이 없어도 이 검증을 먼저 제안한다 .
Mutation 검증
테스트의 단언을 쓰거나 바꾼 뒤, 그 테스트가 실제로 버그를 막는지 변형을 심어
확인합니다. false green — 통과하지만 아무것도 지키지 않는 테스트 — 를 커밋 전에
걸러내는 단계입니다.
좋은 테스트에는 여러 성질이 있습니다. 버그 방어, 리팩터링 내성, 빠른 피드백,
유지보수성. 이 스킬이 재는 건 그중 버그 방어 하나입니다. 나머지는 각 프로젝트의
테스트 규약이 맡습니다. 그래서 모든 판단은 한 질문으로 수렴합니다.
이 테스트는 사용자나 운영에 닿는 중요한 버그를 실제로 막나요?
unit / integration / E2E 어느 레벨이든 같은 절차를 적용합니다. 레벨에 따라 달라지는 건
"관찰 표면이 무엇인가" 하나뿐입니다.
1~3은 변형 하나마다 도는 루프입니다. 여러 변형을 한꺼번에 심으면 무엇이 무엇을
죽였는지 귀속되지 않습니다. 9는 루프를 다 빠져나온 뒤 한 번만 합니다.
0. 관문 — 이 검증이 필요한가
필요합니다: 단언이나 테스트 케이스를 새로 추가했거나 변경했다. 새 보장이 생겼으니
그게 진짜인지 확인해야 합니다.
건너뜁니다: 이름 변경, 서식, 파일 이동처럼 단언을 안 건드린 순수 리팩터링. 검증할
새 보장이 없습니다.
건너뛰는 경우에도 9. 종결 단계로 가서 건너뛴 사실을 기록합니다.
기록 없이 지나가면 "검증했는데 통과"와 "안 했다"가 구분되지 않습니다.
1. 설계 — 어떤 변형을 심을까
변형 후보는 코드 줄이 아니라 버그에서 시작합니다.
"이 조건문을 뒤집으면 깨질까"가 아니라 "이 정책이 깨지면 어떤 버그가 나는가"
이 단계의 상세 — 좋은 변형의 다섯 조건, 레벨별 관찰 표면의 정의, 실패면을 나누는 축,
후보 표 — 는 부록 A. 변형 설계를 읽고 진행하세요. 요약만 옮기면
조건 3(관찰 표면)과 조건 4(실패면 분리)에서 거의 반드시 실수합니다.
이 단계에서 반드시 남겨야 하는 산출물이 하나 있습니다.
테스트의 약속을 한 줄로 적는다
테스트가 보장하려는 문장을 변형을 심기 전에 적습니다.
(unit) 입력 조건 → 출력 계약
일반 요금제가 입력되면, 재고가 있어도 computeDeliveryOption은
standardOnly를 반환해야 한다.
(integration) 사용자 시나리오 → 관찰 결과
당일배송 재고가 있어도, 일반 요금제에서는 당일배송 선택이 막히고
구독 전용 안내가 보여야 한다.
이 한 줄이 3단계 판정에서 **"이 버그가 이 테스트의 약속 안인가 밖인가"**를 가르는
유일한 기준입니다. 여기가 흐릿하면 뒤의 판정이 통째로 불가능해집니다.
2. 실행 — 예측을 적고, 심고, 되돌린다
핵심은 심는 절차가 아니라 결과를 보기 전에 예측을 적는 것입니다. 예측 없이 결과를
보면 무엇이 나오든 말이 되고, 판정은 사후 합리화가 됩니다. 등가 변형 판정은 원래
결정 불가능한 문제라, 판정을 잘하려 하는 대신 틀렸을 때 드러나게 만듭니다.
변형 하나마다:
막으려는 버그와 테스트의 약속을 적는다
변경을 작게, 한 번에 하나만 적용한다
예측을 두 축으로 적는다
무엇이 바뀌는가 — 관찰 표면에서의 결과
어느 테스트가 어떤 실패를 내는가 — 테스트 이름과 실패 모양
테스트를 실행하고, 실제 실패가 예측과 일치하는지 본다
판정 단계으로 분류한다
변형 diff를 즉시 되돌리고, 원래 테스트가 다시 Green인지 확인한다
작업 트리가 깨끗한지 확인한다 — 포맷·import 정렬 diff도 잔여물이다
예측은 대화 컨텍스트가 아니라 파일에 적으세요. 컨텍스트 안에만 있으면 결과를 본 뒤
조용히 수정될 수 있고, 그러면 이 장치 전체가 무의미해집니다.
실패의 범위도 신호다
표면 하나를 찌른 변형이 예상한 테스트 하나가 아니라 전부를 죽였다 → 단언들이
서로 얽혀 있다는 뜻
공통 함수를 찌른 변형이 예상보다 좁게 죽었다 → 어떤 경로가 그 정책을 실제로
타지 않는다는 뜻
변형은 실험이지 구현 변경이 아닙니다. 어떤 경우에도 커밋에 남으면 안 됩니다.
3. 판정 — 분류하고 대응한다
결정 트리, 상황별 해설, 실패 메시지 다듬기, todo 작성법은
부록 B. 판정에 있습니다.
변형이 살아남았다면 반드시 읽고 판정하세요.
여기서는 자주 틀리는 두 가지만 미리 못 박습니다.
"바꿔도 결과가 같은 변형"을 성급히 붙이지 마세요. 트리에서 판정이 가장 어렵고 가장
자주 틀리는 자리입니다. 진짜 동일 동작은 모든 입력에서 결과가 같아야 하는데, 보통은
"지금 이 픽스처에서는 같아 보인다"에 그칩니다. 후자는 등가가 아니라 픽스처가 그 차이를
안 태우고 있는 것입니다.
헷갈리면 "아무도 안 지킴"으로 적습니다. 놓친 버그로 남겨 두는 편이, 안 지키는 것을
지킨다고 착각하는 것보다 항상 안전합니다.
멈출 때
하나의 정책에 대해 서로 다른 실패면 3~5개를 검증했다면 다음 정책으로 넘어갑니다.
숫자가 목표가 아닙니다. "서로 다른" 실패면이 핵심입니다. 같은 단언을 다섯 번 때린
것은 하나로 셉니다.
9. 종결 — 원복과 이행 기록
변형은 그때만 하는 실험이라 코드에 흔적을 남기지 않습니다. 남기는 건 "이 절차대로
검증했다"는 한 줄뿐이고, 커밋 메시지 끝에 trailer로 적습니다.
Mutation-Verified: killed 3 / survived 1(아무도 안 지킴) by mutation-verify
0단계에서 건너뛴 경우도 남깁니다.
Mutation-Verified: skip(no assertion change) by mutation-verify
커밋에 테스트 파일이 있으면 훅이 이 trailer를 확인하고, 없으면 커밋을 막습니다.
killed / survived 숫자는 이력용 기록이지 목표가 아닙니다. 놓침은 분류와 함께
적습니다.
trailer는 검증했다는 표시일 뿐 검증의 질을 보장하지 않습니다. 질은 이 문서의
나머지가 정합니다. 서명만 먼저 쓰고 검증을 건너뛰면 손해는 본인이 봅니다.
커밋 전 최종 점검
변형 diff가 하나도 남아 있지 않은가 (git diff, git status 둘 다)
원래 테스트가 다시 Green인가
관련된 다른 레벨 테스트도 의도대로 통과하는가
커밋 메시지에 Mutation-Verified: trailer를 남겼는가
이 스킬이 만들 수 있는 최악의 결과는 부실한 테스트가 아니라, 일부러 심은 버그가
배포되는 것입니다. 중간에 실패하거나 중단되더라도 원복이 항상 먼저입니다.
최종 원칙
좋은 테스트의 마지막 질문: 이 테스트는 AI가 만든 구현을 신뢰하는 데 실제 근거가
되나요?
좋은 산출물은 잡은 개수가 아니라 다음 넷입니다.
이 테스트가 어떤 버그를 막는지
어떤 실패면을 실제로 잡았는지
어떤 변형이 놓쳤고 왜 놓쳤는지
그래서 이 테스트를 어디까지 믿어도 되는지
부록 A. 변형 설계
본문 1. 설계 단계에서 읽습니다.
좋은 변형의 다섯 조건
좋은 변형은 테스트의 버그 방어력을 실제로 시험하는 변형입니다. 다섯 조건을 모두
만족해야 합니다. 하나라도 어기면 실행해도 정보가 나오지 않습니다.
조건
어기면 생기는 일
1. 실제 버그를 일으킨다
프로덕션에 나가도 아무도 안 다치는 변형. 죽든 살든 알 게 없음
2. 이번 픽스처에서 도달한다
심은 자리가 실행조차 안 됨. 살아남아도 테스트 탓이 아님
3. 관찰 표면에서 결과가 바뀐다
코드는 망가졌는데 그 레벨에서 안 보임. 테스트가 볼 방법이 없음
4. 앞 후보와 실패면이 다르다
같은 단언을 반복해서 때림. 두 번째부터는 새 정보 없음
5. 컴파일된다
실험이 성립하지 않음. 타입 에러는 버그가 아니라 컴파일러의 일
조건 1은 현실 축입니다 — 프로덕션에서 사용자가 다치는가.
조건 3은 테스트 축입니다 — 이 레벨의 관찰 표면에 드러나는가.
둘이 갈리는 자리가 mock에 가려지는 경우입니다. 버그는 실재하는데 가짜 데이터가 버그
경로를 끊어서 테스트 세계에는 나타나지 않습니다. 이 구분은 판정에서 다시 쓰이니
기억해두세요.
관찰 표면이란
관찰 표면은 그 테스트 레벨이 볼 수 있는 결과의 범위입니다.
unit — 반환값(출력 계약)
반환값이 standardOnly에서 sameDay로 바뀜
경계 입력의 출력이 다른 union 멤버로 넘어감
❌ 내부 변수·중간 계산만 바뀌고 반환값은 그대로 → 조건 3 위반
integration — 사용자에게 보이는 화면·플로우·부수 효과
일반 요금제에서 당일배송 옵션이 선택 가능해짐
수수료 안내 화면 없이 다음 단계로 넘어감
feature flag가 off인데 진입 퍼널이 렌더됨
❌ 내부 함수 호출 순서만 바뀌거나 사용자가 못 보는 리팩터링 차이 → 조건 3 위반
콜백 예외 (integration). 화면에 안 보여도 "다음 단계로 넘어간다"는 신호인 콜백은
관찰 표면에 넣습니다. 예를 들어 주문 확정 시 호출되는 onConfirmOrder가 불렸다면,
사용자에게는 다음 화면이 열린 것과 같습니다.
AI가 만드는 변형의 함정
AI가 심는 변형은 실제 버그에 가까워서 약한 테스트를 잘 드러냅니다. 하지만 조건 2·3·4·5를
자주 어깁니다. 실제로 LLM이 생성한 변형은 규칙 기반 도구보다 컴파일 실패율이 훨씬
높다는 보고가 있습니다. 조건 5를 형식적인 항목으로 취급하지 마세요.
특히 죽은 분기가 위험합니다. 많은 JS 테스트 러너는 실행 시 타입 검사를 하지 않기
때문에, 도달하지 않은 변형도 멀쩡히 실행된 뒤 "놓침"으로 기록됩니다. 그러면 테스트가
약하다는 뜻으로 오독하기 쉽습니다.
조건을 만족시키는 순서
1) 막으려는 버그를 고른다 (조건 1)
변형 후보는 코드 줄이 아니라 버그에서 시작합니다.
일반 요금제 사용자가 당일배송 선택 단계로 진입한다.
feature flag가 off인데도 파일럿 퍼널에 진입한다.
추가 배송비가 있는데 확인 화면 없이 주문이 진행된다.
재고가 있는 옵션이 선택 화면에서 사라진다.
할인 임계 금액이 낮아져 회사가 비용을 떠안는다.
변형은 이 버그를 실제로 일으키는 방향이어야 합니다.
2) 테스트의 약속을 적는다
SKILL의 1단계에 형식이 있습니다. 이 문장이 판정 트리에서 **"약속 안인가 밖인가"**를
가르는 기준이 됩니다. 여기서 흐릿하면 판정이 불가능해집니다.
3) 서로 다른 실패면으로 나눈다 (조건 4)
같은 단언을 반복해서 때리지 않습니다. 한 정책에 연결된 여러 지점을 나눠서 찌릅니다.
실패면의 모양은 레벨마다 다릅니다.
unit — 한 함수 안의 서로 다른 정책 조항
축: 게이트별 조건 / 경계값 / alias 정규화 그룹 / 게이트 간 격리
정책: "일반 요금제는 당일배송이 막혀야 한다"
허용 목록에 일반 요금제를 추가 (가드 무력화)
재고 경계 > 0을 >= 0으로 이동
alias 정규화 그룹에서 멤버 하나 누락
다른 게이트의 입력을 참조 (게이트 누설)
integration — 정책이 지나는 layer 사슬
API 응답 해석 → 재고·요금제 상태 계산 → 정책 계산
→ 선택된 옵션 상태 연결 → UI 분기 → 실제 확정 동작 차단
정책: "일반 요금제는 당일배송이 막혀야 한다"
당일배송 재고를 available로 잘못 해석
구독 전용 정책 조건 제거
선택된 요금제 대신 다른 상태를 참조
안내는 띄우지만 return을 빼서 확정 콜백도 함께 호출
standardOnly 분기를 soldOut처럼 처리
특별한 이유가 없으면 첫 후보는 가드 무력화로 잡습니다. 정책의 차단 조건이 아예
실행되지 않게 만드는 변형입니다. 실제 버그에서 가장 흔한 모양이 "가드가 조용히 죽는"
것이기 때문입니다.
공통 함수(helper)를 둔 정책은 helper와 호출 지점(callsite)이 서로 다른 질문에
답합니다.
helper 변형 → "정책 자체가 맞는가"
callsite 변형 → "각 사용자 경로가 그 정책을 실제로 호출하는가"
unit에서는 helper만 찌릅니다. callsite 질문은 integration의 책임이라, callsite
변형이 살아남으면 "다른 테스트가 지킴"으로 분류합니다.
integration에서는 둘을 나눠서 모두 찌릅니다.
4) 후보 표에서 고른다
✅ 좋은 후보 (실제 버그로 연결)
❌ 효과 낮은 후보
정책 조건 제거·반전
단순 문구 변경
API 응답 필드 오해
타입·빌드 에러만 내는 변경
선택한 상태 대신 다른 상태 참조
현실적이지 않은 무작위 코드 삭제
안내 UI는 뜨지만 진행도 함께 발생
사용자가 못 보는 내부 리팩터링 차이
feature flag off인데 진입이 열림
현재 테스트 책임 밖 정책의 억지 검증
추가 비용이 있는데 확인 단계 건너뜀
이미 같은 실패면을 검증한 변형의 반복
재고가 있는데 옵션 필터에서 누락
실패 응답을 성공처럼 처리
경계값 이동 (> ↔ >=)
alias 정규화 그룹에서 멤버 누락
문구 변경은 특히 조심합니다. 법적·고지성 문구가 아니면 대부분 유지보수 비용만 늘립니다.
5) 심기 전에 검산한다 (조건 2·5)
도달은 프로그램으로 확인할 수 있습니다. 분기(branch) 커버리지를 보세요.
라인 커버리지로는 부족합니다. 조건문 줄은 매번 실행돼도, 변형이 여는 반대편 팔은 한 번도
안 열렸을 수 있습니다.
# 예 (러너에 맞게 조정)
vitest run <테스트파일> --coverage.enabled
pytest <테스트파일> --cov --cov-branch
한 번 돌리면 후보 전체를 스크리닝할 수 있습니다. 나머지는 손으로 자문합니다.
이 분기가 이번 시나리오에서 열리나 (조건 2 — 위 명령으로 확인)
컴파일은 되나 (조건 5)
앞 후보와 실패면이 겹치나 (조건 4)
관찰 표면에서 결과가 진짜 바뀌나 (조건 3)
조건 3은 도구가 못 걸러냅니다. "실행은 되는데 결과가 안 보이는" 경우라서, 판정
단계에 위임합니다.
부록 B. 판정
본문 3. 판정 단계에서 읽습니다.
결정 트리
변형 실행 결과
│
├─ 잡힘(killed)
│ 대응 → 실패 메시지가 정책 언어로 읽히는지 확인.
│ 안 읽히면 단언을 다듬는다 (아래 「실패 메시지 다듬기」)
│
└─ 놓침(survived)
│
├─ 실행조차 안 됨 = 도달 안 함
│ 대응 → 무정보. 버리고 다른 자리에 다시 심는다.
│ 놓침으로 세지 않는다.
│
└─ 실행됨
│
├─ 모든 입력에서 결과가 같음 = 바꿔도 결과가 같은 변형
│ 대응 → 없음.
│ "이번 픽스처에서만 같음"과 혼동 주의 (아래 해설)
│
└─ 입력에 따라 결과가 달라짐 = 버그 실재
│
├─ 이 테스트의 약속 밖
│ ├─ 다른 테스트가 지킴
│ │ 대응 → 없음
│ └─ 아무도 안 지킴
│ 대응 → todo로 미래 테스트의 약속 한 줄
│
└─ 이 테스트의 약속 안
├─ 단언을 안 걸었음
│ 대응 → 지금 단언을 추가·강화한다
└─ mock 데이터에 가려짐
대응 → 지금 mock을 현실에 가깝게 고친다
대응은 아래로 갈수록 무거워집니다. 위쪽은 할 일이 없거나 다음 회차로 넘기지만,
맨 아래 둘은 커밋 전에 지금 고쳐야 합니다.
"약속"은 1단계에서 적어둔 그 한 줄입니다. 그게 없으면 이 트리를 탈 수 없습니다.
상황별 해설
잡힘(killed)
테스트가 심어둔 버그를 잡았습니다. 끝이 아닙니다. 실패 메시지가 원인을 못 알려주면,
진짜 버그가 났을 때 동료가 CI 로그만 보고 원인을 찾지 못합니다.
도달 안 함
심은 자리가 이번 픽스처에서 실행되지 않았습니다. 테스트가 못 잡은 게 아니라 잡을
기회가 없었던 것이라, 놓침으로 세면 안 됩니다. 분류가 아니라 무효 실험입니다.
다른 자리에 다시 심습니다.
바꿔도 결과가 같은 변형
어떤 입력을 넣어도 결과가 같은 변형입니다.
성급히 붙이지 마세요. 이 트리에서 판정이 가장 어렵고 가장 자주 틀리는 자리입니다.
진짜 동일 동작은 모든 입력에서 결과가 같아야 하는데, 실제로는 "지금 이 픽스처에서는
결과가 같아 보인다"에 그치는 경우가 대부분입니다. 후자는 등가가 아니라 픽스처가 그
차이를 안 태우고 있는 것이라, "아무도 안 지킴"이거나 케이스 부족입니다.
둘 사이에서 헷갈리면 "아무도 안 지킴"으로 적습니다. 놓친 버그로 남겨 두는 편이, 안
지키는 것을 지킨다고 착각하는 것보다 안전합니다.
다른 테스트가 지킴
버그는 실재하지만 이 테스트의 약속 밖이고, 이미 다른 테스트가 책임집니다. 예를 들어
unit에서 callsite 변형이 살아남으면 integration이 그 질문을 맡습니다. 할 일이 없습니다.
아무도 안 지킴
버그는 실재하고 중요한데 지금 어떤 테스트도 보고 있지 않습니다. 이 테스트가 보장하지
않는 범위를 드러낸 것이라, 놓침 중 가장 값진 결과입니다. 변형을 심지 않았으면
영영 몰랐을 구멍입니다. 부채로 등록합니다.
단언을 안 걸었음
버그가 실재하고, 약속 안이고, 관찰 표면에도 드러나는데, 그걸 보는 단언이 없어서
통과했습니다. 이 테스트의 결함입니다. 분류만 하고 넘어가지 말고 지금 단언을
추가합니다.
mock 데이터에 가려짐
버그가 실재하고 약속 안인데, 가짜 데이터가 버그 경로를 끊어 테스트 세계에 나타나지
않습니다. 이 트리에서 유일하게 테스트 구조 자체가 잘못됐다는 신호입니다.
보통 과잉 mock의 증상입니다. 직접 책임지는 코드까지 가짜로 바꾸지 않았는지 확인하세요.
어디까지 mock해도 되는지는 프로젝트의 테스트 규약을 따릅니다.
실패 메시지 다듬기
잡았는지만 보면 부족합니다. 실패 메시지만 보고 어떤 버그가 났는지 이해할 수 있어야
합니다.
✅ 좋은 실패 (unit — 행 이름 + 리터럴 diff가 정책 언어)
FAIL ... > 일반 요금제는 재고가 있어도 standardOnly다
expected 'sameDay' to be 'standardOnly'
✅ 좋은 실패 (integration — 메시지가 정책 언어)
Unable to find role="heading" name="당일배송은 구독 회원만 이용할 수 있어요"
expected onConfirmOrder not to have been called, but it was called with a standard-plan payload
❌ 나쁜 실패 메시지 (레벨 공통 — 원인 불명)
Unable to find element
expected false to be true
Cannot read properties of undefined
나쁜 메시지는 기록하고 넘어갈 관찰이 아니라 테스트를 고치라는 신호입니다.
실패 메시지는 변형이 아니라 테스트 단언이 결정합니다. 지금 이 변형이 만든 메시지는,
나중에 진짜 버그가 났을 때 동료가 CI에서 보게 될 메시지와 같습니다. 커밋 전에 단언을
다듬으세요.
unit — 정책 언어는 단언 메시지가 아니라 테스트 이름에 삽니다.
여러 케이스가 한 it에 몰려 어떤 입력이 뚫렸는지 안 보이면, 케이스를 테이블 기반
(it.each 등) 행으로 펼쳐서 실패한 행 이름이 곧 정책 메시지가 되게 합니다.
integration
앵커 단언이 정책 단언을 가려서 실패한다면, 앵커를 정책과 무관하게 항상 렌더되는
요소로 교체합니다. (앵커를 부정 단언보다 앞에 두는 순서 자체는 건전성 조건이므로
유지합니다.)
원시값 비교가 expected false to be true를 낸다면, 단언에 정책 메시지를 실어줍니다.
expect(값, '배너 클릭은 영구 저장돼야 한다')
「단언을 안 걸었음」의 대응도 여기로 옵니다. 하나는 단언이 아예 없는 것이고 다른 하나는
단언이 뭉툭한 것이라, 둘 다 단언의 결함입니다.
안 지켜지는 버그를 todo로
"아무도 안 지킴"으로 분류한 놓침은 그 자리에서 todo로 남깁니다. 러너가 제공하는
빈 테스트 문법(it.todo, test.todo, @skip 등)을 씁니다.
todo 메시지에는 발견 경위가 아니라 미래 테스트의 약속을 적습니다. 맥락 없이 이
파일을 여는 다음 사람이 본문만 채우면 되도록 씁니다.
// ❌ 발견 경위 — 무엇을 단언할지 알 수 없다it.todo('상태 계산 변형이 살아남음');// ✅ 약속 형식 — 이대로 본문만 채우면 된다it.todo('당일배송 재고가 없는 옵션은 선택 화면에 노출되지 않는다');
약속 한 줄이 안 써지면 "아무도 안 지킴"이 아니라 "다른 테스트가 지킴"입니다.
todo 작성이 분류의 검산을 겸합니다.