본문으로 건너뛰기
중급약 24분

구조 기반 기법 — 구문·분기·조건·MC/DC 커버리지

명세가 아니라 실제 코드를 근거로 테스트한다. 구문·분기·조건 커버리지의 차이를 코드로 직접 계산해본다.

  • #구조기반
  • #화이트박스
  • #커버리지
  • #MC/DC

개념

명세를 보는 눈에서 코드를 보는 눈으로

지금까지 배운 기법(동등분할, 경계값, 상태전이, 결정테이블, 유즈케이스, 페어와이즈, 분류 트리)은 전부 명세 기반 이다 — 코드를 한 줄도 안 보고 만들 수 있다. 구조 기반 기법은 반대다. 실제 코드의 구조를 보고, 그 구조가 얼마나 실행됐는지를 기준으로 테스트를 설계하거나 평가한다.

비유하자면 이렇다. 명세 기반 기법은 여행 계획서를 보고 "가기로 한 곳을 다 갔는가"를 확인하는 일 이다. 구조 기반 기법은 지도를 펼쳐놓고 "이 도시의 길 중 우리가 밟지 않은 길은 어디인가"를 확인하는 일 이다. 계획서에 없던 골목이 지도에는 있을 수 있고, 사고는 대개 그 골목에서 난다.

여기서 아주 중요한 전제를 먼저 못박아 두자. 커버리지는 테스트를 만들어내는 도구가 아니라, 이미 만든 테스트를 평가하는 자다. 커버리지 숫자를 보고 "여기가 비었네"를 알 수는 있지만, 그 빈칸을 무엇으로 채워야 하는지는 커버리지가 말해주지 않는다. 채우는 것은 여전히 명세 기반 기법과 사람의 판단이다. 이 구분을 놓치면 이 레슨의 마지막 절("커버리지 100%가 결함 0%가 아닌 이유")이 왜 그렇게 중요한지 이해할 수 없다.

커버리지는 무엇을 재는 숫자인가

커버리지 도구는 마술이 아니다. 하는 일은 단순하다. 코드의 각 줄과 각 분기에 "여기 지나갔음"을 기록하는 카운터를 심어 두고(계측, instrumentation), 테스트를 돌린 뒤 카운터를 세는 것 이다.

그래서 커버리지가 말해줄 수 있는 것과 없는 것이 처음부터 정해져 있다.

커버리지가 아는 것커버리지가 모르는 것
이 줄이 실행됐다그 줄의 결과가 맞는지
이 분기의 참·거짓이 각각 나왔다그 분기의 조건이 기획서와 같은지
이 함수가 한 번도 호출되지 않았다있어야 할 함수가 아예 없다는 것

오른쪽 열이 이 레슨의 절반이다. 왼쪽만 보고 안심하는 순간이 실무에서 사고가 나는 지점이다.

하나의 함수로 세 기준을 나란히 계산하기

말로 설명하면 다 비슷하게 들린다. 같은 함수 하나로 세 기준을 직접 계산 해 보면 차이가 확실해진다.

function getShippingFee(amount, isMember) {
  if (amount >= 30000 && isMember) {
    return 0;            // ① 무료배송
  }
  return 3000;           // ② 배송비 3000원
}

이 함수에는 판정(decision)이 하나, 그 안에 개별 조건(condition)이 두 개(amount >= 30000, isMember) 있다. 가능한 조건 조합은 네 가지다.

#amount >= 30000isMember판정 결과실행되는 줄
1참참참①
2참거짓거짓②
3거짓참거짓②
4거짓거짓거짓②

이제 세 기준이 각각 무엇을 요구하는지 보자.

구문 커버리지(Statement Coverage) — 모든 실행 가능한 줄이 최소 한 번씩 실행됐는가. ①과 ② 두 줄이 다 실행되면 된다. 조합 1번 하나(①)와 2·3·4번 중 아무거나 하나(②), 즉 두 개의 케이스면 100% 다.

분기 커버리지(Branch/Decision Coverage) — if 판정의 결과가 참·거짓 각각 한 번씩 나왔는가. 위와 같은 두 케이스로 참·거짓이 다 나오므로 여기서도 두 개면 100% 다. 이 함수처럼 if에 else가 없더라도 "판정이 거짓이라 아무것도 안 하고 지나간 경로"가 분기 하나로 세어진다는 점을 기억해두자.

조건 커버리지(Condition Coverage) — amount >= 30000과 isMember 각각이 참·거짓을 모두 거쳤는가. 조합 2번(참·거짓)과 3번(거짓·참) 두 개면 두 조건 모두 참·거짓을 다 겪는다. 역시 두 개면 100% 다.

여기서 이 레슨에서 가장 놀라운 사실이 나온다.

조건 커버리지 100%를 만족하는 그 두 케이스(2번·3번)는 판정 결과가 둘 다 거짓이다. 즉 return 0;(①)은 한 번도 실행되지 않았고, 분기 커버리지는 50%이며, 구문 커버리지도 50%다.

세 기준을 한 표로 정리하면 이렇다.

기준최소 케이스 예시그때 구문그때 분기그때 조건
구문 100%(50000, true), (10000, true)100%100%50%(isMember가 계속 참)
분기 100%(50000, true), (10000, true)100%100%50%
조건 100%(50000, false), (10000, true)50%50%100%
조건+분기 100%위 두 묶음의 합 = 3케이스100%100%100%

"더 엄격한 기준이 항상 약한 기준을 포함한다"는 직관이 여기서 깨진다. 조건 커버리지는 분기 커버리지를 보장하지 않는다. 그래서 실무 도구들은 조건 커버리지를 단독으로 쓰지 않고, 조건/판정 커버리지(Condition/Decision Coverage) — 개별 조건도 참·거짓을 다 겪고 판정 결과도 참·거짓을 다 겪어야 하는 기준 — 을 쓴다.

단축 평가가 커버리지를 한 번 더 흔든다

대부분의 언어에서 A && B는 A가 거짓이면 B를 아예 평가하지 않는다. 이걸 단축 평가(short-circuit)라고 한다. 위 표의 3번 조합(amount >= 30000이 거짓)에서 isMember는 값이 무엇이든 읽히지도 않는다.

QA에게 이게 왜 중요한가.

  • isMember를 만드는 코드가 예외를 던지는 코드였다면, 그 예외는 amount가 작은 경우엔 영원히 재현되지 않는다. "가끔만 터지는 버그"의 흔한 정체다.
  • 커버리지 도구는 "평가되지 않은 조건"을 미커버로 표시한다. 리포트에서 분기 퍼센트가 이상하게 낮게 나오는 이유가 대개 여기다.
  • 개발자가 &&의 순서를 바꾸는 리팩터링(예: isMember && amount >= 30000)을 하면 동작은 같아 보이지만 실행 경로가 달라진다. 회귀 테스트에서 눈여겨볼 변경이다.

모듈 9(코드 읽기)에서 &&·||를 읽는 법을 다시 만나게 되는데, 그때 이 단축 평가를 떠올리면 "조건의 순서" 자체가 테스트 대상이 된다는 걸 알게 된다.

MC/DC — 왜 여기까지 가는가

MC/DC(Modified Condition/Decision Coverage) 는 조건/판정 커버리지에 한 가지를 더 요구한다.

각 개별 조건이, 다른 조건은 그대로 둔 채 자기 값만 바뀌었을 때, 판정 결과를 바꾼다는 것을 보이는 케이스 쌍이 있어야 한다.

말이 어려우니 위 표로 직접 골라보자. isMember가 독립적으로 영향을 준다는 걸 보이려면 — amount >= 30000은 고정한 채 isMember만 뒤집었을 때 판정이 바뀌는 쌍이 필요하다. 조합 1번(참·참 → 참)과 2번(참·거짓 → 거짓) 이 그 쌍이다. amount >= 30000에 대해서는 1번과 3번(거짓·참 → 거짓) 이 쌍이 된다.

조건독립성을 보이는 쌍무엇이 증명되는가
isMember1번 ↔ 2번금액이 3만원 이상일 때, 회원 여부만으로 결과가 갈린다
amount >= 300001번 ↔ 3번회원일 때, 금액만으로 결과가 갈린다

두 쌍을 합치면 필요한 케이스는 1번, 2번, 3번 세 개 다. 4번(거짓·거짓)은 필요 없다. 조건이 n개일 때 MC/DC를 만족하는 최소 케이스 수는 보통 n+1개 로, 모든 조합을 다 도는 2ⁿ개보다 훨씬 적다. 조건이 10개면 1,024개가 아니라 11개다. MC/DC의 실용적 가치가 바로 이 지점 이다 — 전수 조합의 폭발을 피하면서도 "각 조건이 정말 쓸모 있는가"를 증명한다.

MC/DC가 잡아내는 대표적인 결함이 하나 있다. 아무 영향도 주지 않는 조건. 개발자가 if (a && b && (c || c))처럼 논리를 잘못 쓰거나, 요구사항이 바뀌면서 의미를 잃은 조건이 남아 있으면, 그 조건은 어떤 케이스로도 독립적 영향을 보일 수 없다. 즉 MC/DC를 만족시키려다 실패하는 것 자체가 결함 발견 이 된다.

왜 항공·의료에서는 여기까지 요구하는가

표준산업최고 등급에서 요구하는 커버리지
DO-178C항공 (기내 소프트웨어)Level A에서 MC/DC
ISO 26262자동차ASIL D에서 MC/DC
IEC 62304 / FDA 지침의료기기위험 등급에 따라 분기 이상
EN 50128철도 신호SIL 4에서 MC/DC 권고

공통점은 하나다. 실패의 결과가 사람의 생명이고, 되돌릴 수 없으며, 핫픽스를 배포할 수 없다. 비행 중인 항공기에 앱스토어 업데이트를 밀어 넣을 수는 없다. 이런 영역에서는 "발견 못 한 결함 하나의 기대 손실"이 사실상 무한대에 가깝기 때문에, 검증 비용이 개발 비용의 몇 배가 되더라도 정당화된다. 인증기관에 증거를 제출해야 한다는 점도 크다 — MC/DC 리포트는 품질 활동이기 이전에 규제 문서다.

왜 일반 웹·앱 서비스는 여기까지 안 가는가

같은 계산을 일반 서비스에 해보면 답이 뒤집힌다.

축안전 필수 시스템일반 웹·앱 서비스
결함 1건의 최악 결과인명 사고사용자 이탈, 환불, 사과문
수정 배포까지 걸리는 시간몇 개월(재인증 포함)몇 시간
요구사항 변경 주기수년수 주
MC/DC 확보 비용정당화됨케이스 수보다 유지보수 비용 이 문제

마지막 줄이 실무에서 가장 결정적이다. MC/DC 케이스를 한 번 만드는 비용보다, 2주마다 로직이 바뀔 때 그 케이스들을 계속 고쳐 나가는 비용 이 훨씬 크다. 요구사항이 자주 바뀌는 제품에서 MC/DC를 강제하면, 팀은 곧 테스트를 고치는 데 지쳐 테스트를 지우기 시작한다. 그래서 일반 서비스에서는 분기 커버리지를 기본선으로 두고, 결제·정산·권한·개인정보처럼 틀렸을 때 비싼 모듈에만 조건/판정 수준을 요구하는 선택적 적용이 현실적인 답이다.

"어디까지 갈 것인가"는 기술 판단이 아니라 위험 판단 이라는 점을 기억하자. 이건 모듈 4(테스트 계획·리스크)에서 배울 위험 기반 테스트의 사고방식과 정확히 같다.

커버리지 100%가 결함 0%를 의미하지 않는 이유 — 코드로 보기

말로만 들으면 "그래도 100%면 어느 정도 안심 아닌가" 싶다. 직접 보자.

function getFinalPrice(price, discount) {
  if (discount > 0) {
    return price + discount;   // 버그: 빼야 하는데 더하고 있다
  }
  return price;
}

이 함수에 붙은 테스트다.

test('할인이 있으면 값이 계산된다', () => {
  expect(getFinalPrice(10000, 1000)).toBeGreaterThan(0);
});
 
test('할인이 없으면 원래 가격', () => {
  expect(getFinalPrice(10000, 0)).toBe(10000);
});

두 테스트를 돌리면 구문 커버리지 100%, 분기 커버리지 100%, 조건 커버리지 100% 가 나온다. 리포트는 온통 초록색이다. 그런데 1,000원 할인 쿠폰을 쓰면 가격이 1,000원 오른다. 첫 테스트의 어서션이 toBeGreaterThan(0)이라 11,000도 통과시켰기 때문이다.

핵심은 이것이다. 커버리지는 "실행"을 재고, 결함 발견은 "검증"에서 나온다. 어서션이 약하면 커버리지는 100%인 채로 결함이 그대로 통과한다. 극단적으로는 어서션이 하나도 없는 테스트(그냥 함수를 호출만 하는 테스트)도 커버리지 100%를 만들 수 있다.

커버리지가 구조적으로 못 잡는 결함을 정리하면 이렇다.

결함 유형예시왜 커버리지에 안 걸리나
약한 어서션위 예시줄은 실행됐다
어서션 없음호출만 하는 테스트실행 카운터는 올라간다
누락된 코드기획서엔 있는 "쿠폰 중복 사용 금지"가 구현 안 됨없는 줄은 셀 수 없다
잘못된 경계> 를 써야 할 자리에 >=그 줄은 어느 쪽이든 실행된다
잘못된 상수3만원 기준을 5만원으로 씀조건문은 정상 실행된다
상태·순서 결함두 번 연속 클릭, 뒤로가기 후 재제출각 줄은 이미 다 실행됐다
동시성 결함같은 재고를 동시에 주문실행 순서는 커버리지가 안 본다
비기능 결함응답이 8초 걸림, 권한 없이 접근됨성능·보안은 측정 대상이 아니다

이 표에서 "누락된 코드" 줄이 가장 무섭다. 커버리지는 존재하는 코드에 대해서만 계산되므로, 구현되지 않은 요구사항은 분모에 아예 들어가지 않는다. "요구사항 커버리지"(모듈 2·4에서 다루는 추적성)와 코드 커버리지가 전혀 다른 숫자인 이유가 여기 있다.

정리하면 한 줄이다. 커버리지는 좋은 테스트의 필요조건이지 충분조건이 아니다. 커버리지가 낮으면 확실히 문제가 있고, 커버리지가 높다고 문제가 없는 건 아니다.

QA가 커버리지 리포트를 읽는 법

실무에서 QA가 커버리지를 마주치는 자리는 대개 정해져 있다. PR에 붙은 CI 코멘트, 또는 개발자가 공유한 리포트 링크 다. JavaScript는 Istanbul(nyc, Jest의 --coverage), Java는 JaCoCo, Python은 coverage.py가 대표적이고, 이 결과를 SonarQube가 모아 보여주는 구성이 흔하다.

Istanbul의 텍스트 리포트는 이렇게 생겼다.

File            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files       |   82.4  |   61.2   |   88.0  |   82.1  |
 cart.js        |   95.0  |   90.0   |  100.0  |   95.0  | 42
 payment.js     |   48.3  |   22.5   |   50.0  |   47.9  | 15-31,55-70
 utils/date.js  |  100.0  |  100.0   |  100.0  |  100.0  |

QA가 여기서 봐야 할 것은 맨 윗줄의 82.4가 아니다.

볼 것왜
파일별 편차총계 82.4는 date.js의 100%가 끌어올린 숫자다. 정작 payment.js가 48.3이다
% Branch 열분기가 구문보다 항상 낮다. payment.js는 22.5% — 예외·실패 경로가 거의 안 밟혔다는 뜻이다
두 열의 격차구문 48.3 vs 분기 22.5처럼 벌어지면 "정상 경로만 테스트했다"는 신호다
Uncovered Line #s진짜 정보는 여기다. 그 줄을 열어보면 대개 환불·실패·타임아웃 처리다
변경분 커버리지이번 PR에서 새로 추가·수정된 줄이 커버됐는가. 전체 숫자보다 훨씬 유용하다

그리고 QA가 실제로 던질 질문은 이런 모양이 된다. 숫자를 지적하는 게 아니라 미커버 구간이 무엇인지를 묻는 것 이 요령이다.

리포트에서 본 것QA가 던질 질문
payment.js 15-31 미커버"15~31줄이 결제 실패 처리 같은데, 이 경로는 어떻게 검증하고 계신가요?"
분기 22.5%"예외 경로가 대부분 안 밟힌 것 같습니다. 제가 수동으로 커버할 목록을 만들까요?"
총계는 82%인데 신규 파일이 0%"이번에 추가된 모듈에 테스트가 아직 없는 것 같은데, 릴리즈 전에 붙일 계획인가요?"
100%인 파일"이 파일은 100%인데, 어서션이 결과값까지 확인하는지 한 번 같이 볼 수 있을까요?"

마지막 줄이 이 레슨의 앞 절과 이어진다. 100%라는 숫자를 보면 안심할 게 아니라, 그 100%가 어떤 어서션 위에 서 있는지를 봐야 한다.

"커버리지 80%"라는 숫자를 믿으면 안 되는 이유

팀 목표로 "커버리지 80%"를 거는 일이 흔하다. 이 숫자 하나만 보고 안심하면 안 되는 이유가 네 가지다.

  1. 분모가 조작 가능하다. 자동 생성 코드, 설정 파일, 타입 정의를 측정 대상에 포함하거나 제외하는 것만으로 퍼센트가 몇십 씩 움직인다. "무엇을 세지 않기로 했는가"가 숫자보다 중요하다.
  2. 쉬운 코드로 채운 80%가 있다. 날짜 포맷 유틸 같은 코드는 테스트가 쉬워서 금방 100%가 된다. 그 100%들이 총계를 끌어올리는 동안 결제 모듈은 40%에 머문다.
  3. 지표가 목표가 되면 왜곡된다. 커버리지를 KPI로 걸면 어서션 없는 테스트, 의미 없는 호출 테스트가 늘어난다. 모듈 2(레슨 2)에서 본 대리 지표의 함정 이 그대로 재현되는 사례다.
  4. 통합·E2E 테스트가 우연히 지나간 줄도 커버로 잡힌다. 로그인 한 번 하면 수십 개 파일이 실행되지만, 그게 그 코드를 "검증했다"는 뜻은 아니다.

그래서 실무에서 쓸 만한 기준은 총계 퍼센트가 아니라 이 세 가지다.

  • 변경분 커버리지(diff coverage) — 이번에 바뀐 줄만 대상으로 본다. 기존 레거시의 낮은 커버리지에 발목 잡히지 않으면서, 새 코드의 품질은 지킬 수 있다.
  • 모듈별 차등 기준 — 결제·인증·정산은 높게, 화면 표시 코드는 낮게.
  • 커버리지 하락 금지 — 절대값보다 "이번 PR로 떨어지지 않았는가"가 관리하기 쉽고 논쟁도 적다.

도구

SonarQube는 구문·분기 커버리지 결과를 모아 추세와 임계값(Quality Gate)으로 보여주는 대중적인 도구다. 실제 측정은 언어별 도구(Istanbul, JaCoCo, coverage.py)가 하고 SonarQube는 그 결과를 받아 표시하는 구조라는 점을 알아두면 리포트 출처를 헷갈리지 않는다.

VectorCAST, Test RealTime, LDRA 같은 도구는 MC/DC까지 측정하고 인증 제출용 증거 문서 를 생성할 수 있어 안전이 중요한 임베디드·항공·자동차 산업에서 주로 쓰인다. 일반 웹 서비스 팀에서 이 도구들을 볼 일은 거의 없지만, "왜 저런 도구가 따로 존재하는가"를 알면 앞서 본 비용 판단이 자연스럽게 이해된다.

실무에서 왜 필요한가

명세 기반 기법만으로는 "코드에 있지만 요구사항 문서엔 언급 안 된 경로" 를 놓칠 수 있다. 개발자가 방어적으로 짜둔 에러 핸들링 분기, 성능을 위해 넣은 캐시 분기, 과거 요구사항의 잔재로 남은 조건 — 이런 건 명세서 어디에도 안 적혀 있다. 구조 기반 기법은 이 골목들을 드러낸다. 반대로 구조 기반만 쓰면 "코드가 원래 요구사항을 제대로 구현했는가"는 못 본다. 이 모듈 레슨 2에서 강조했듯 두 갈래는 항상 함께 쓴다.

조금 더 구체적으로, 커버리지를 읽을 줄 아는 QA가 실무에서 만드는 차이는 네 가지다.

첫째, 테스트 공백의 위치를 지목할 수 있다. "테스트가 부족한 것 같아요"는 의견이지만, "payment.js 15~31줄, 결제 실패 처리 경로가 미커버입니다"는 사실이다. 사실로 쓰인 지적은 반박당하지 않는다 — 모듈 6(결함 리포트)에서 배운 원칙이 여기서도 그대로 통한다.

둘째, 수동 테스트의 우선순위가 잡힌다. 단위 테스트가 안 닿는 구간이 드러나면, 그 구간이 곧 수동·탐색적 테스트가 반드시 커버해야 할 목록 이 된다. 커버리지 리포트는 개발자만 보는 문서가 아니라 QA의 테스트 범위 설계 입력값이다.

셋째, 릴리즈 판단의 근거가 된다. "이번 릴리즈의 변경분 커버리지가 30%이고, 미커버 구간이 환불 로직입니다"는 릴리즈 회의에서 쓸 수 있는 근거다. 감이 아니라 구조적 사실이기 때문이다.

넷째, "커버리지 높이자"는 요구를 올바른 방향으로 돌릴 수 있다. 숫자만 올리는 방식과 위험한 경로부터 채우는 방식은 결과가 완전히 다르다. 이 차이를 설명할 수 있는 사람이 팀에 QA밖에 없는 경우가 많다.

채용시장에서의 위치

화이트박스 커버리지는 QA 면접에서 개념 확인용 단골 질문 이다. 자주 나오는 형태는 이렇다.

  • "구문 커버리지와 분기 커버리지의 차이를 설명해보세요."
  • "분기 커버리지 100%인데 조건 커버리지가 100%가 아닐 수 있나요? 반대는요?"
  • "커버리지 100%면 버그가 없다고 볼 수 있나요?"
  • "우리 팀 커버리지가 80%인데, QA로서 이 숫자를 어떻게 보시겠어요?"

두 번째 질문이 변별력이 높다. 이 레슨의 표 하나만 떠올리면 "조건 커버리지 100%인데 분기 커버리지가 50%인 경우가 실제로 있습니다" 라고 예를 들어 답할 수 있고, 이건 외운 정의를 말하는 지원자와 확실히 구분된다.

네 번째 질문에서 면접관이 기대하는 답은 "80%면 낮습니다/높습니다"가 아니다. "총계보다 파일별 편차와 분기 커버리지, 그리고 변경분 커버리지를 보겠습니다" 가 좋은 답이다. 숫자를 평가하는 사람이 아니라 숫자의 구조를 아는 사람 으로 보이기 때문이다.

실습 과제

과제 1 — 세 가지 커버리지 직접 계산하기 (15분)

다음 함수에 대해 is_eligible(20, True) 딱 한 번만 테스트했다고 하자.

def is_eligible(age, has_id):
    if age >= 18 and has_id:
        return True
    return False

① 구문 커버리지 ② 분기 커버리지 ③ 조건 커버리지가 각각 몇 %인지 계산하고, 세 기준을 모두 100%로 만들려면 어떤 호출을 추가해야 하는지 적는다.

해설 보기

is_eligible(20, True)는 판정이 참이라 return True만 실행된다.

  • 구문 커버리지 50% — return False 줄이 실행되지 않았다.
  • 분기 커버리지 50% — 판정의 거짓 경로가 나오지 않았다.
  • 조건 커버리지 50% — age >= 18은 참만, has_id도 참만 겪었다. 각 조건이 참·거짓을 다 겪어야 하는데 절반만 채웠다.

추가할 호출의 한 가지 답은 is_eligible(20, False)와 is_eligible(15, True)다. 이 둘을 더하면 세 기준이 모두 100%가 된다(총 3케이스). is_eligible(15, False)는 추가해도 커버리지를 더 올리지 못한다 — 이미 나온 정보만 반복하기 때문이다. "케이스를 늘리는 것"과 "커버리지를 올리는 것"이 같은 말이 아니라는 걸 여기서 느끼는 게 이 과제의 목적이다.

과제 2 — 조건 커버리지의 함정 재현하기 (12분)

같은 is_eligible 함수에 대해, 조건 커버리지는 100%인데 분기 커버리지는 50%인 테스트 집합 을 직접 만들어본다. 케이스는 두 개면 충분하다.

해설 보기

is_eligible(20, False)와 is_eligible(15, True) 두 개다.

  • age >= 18: 첫 호출에서 참, 둘째 호출에서 거짓 → 다 겪음
  • has_id: 첫 호출에서 거짓, 둘째 호출에서 참 → 다 겪음
  • 그런데 판정 결과는 두 번 다 거짓 이라 return True는 한 번도 실행되지 않는다.

즉 조건 커버리지 100%, 분기 커버리지 50%, 구문 커버리지 50%. 이 조합을 스스로 만들어냈다면 "더 엄격해 보이는 기준이 약한 기준을 항상 포함하지는 않는다"를 체감한 것이다. 실무 도구가 조건 커버리지 단독이 아니라 조건/판정 커버리지 를 쓰는 이유가 정확히 이것이다.

과제 3 — MC/DC 케이스 고르기 (15분)

다음 판정에 대해 MC/DC를 만족하는 최소 케이스 집합을 고른다. 조합 표를 먼저 그리고, 각 조건마다 "다른 조건은 고정한 채 자기만 뒤집어 결과를 바꾸는 쌍"을 찾는 순서로 접근한다.

if (isAdmin || hasPermission) {
  // 접근 허용
}
해설 보기

조합 표는 이렇다.

#isAdminhasPermission판정
1참참참
2참거짓참
3거짓참참
4거짓거짓거짓
  • isAdmin의 독립성: hasPermission을 거짓으로 고정하고 isAdmin만 뒤집는 2번 ↔ 4번 쌍.
  • hasPermission의 독립성: isAdmin을 거짓으로 고정하고 뒤집는 3번 ↔ 4번 쌍.

합치면 2번, 3번, 4번 세 개 다. 조건 2개에 n+1=3개라는 규칙과 맞는다. 그리고 1번(둘 다 참)은 필요 없다. AND일 때는 "둘 다 참"이 필수였는데 OR에서는 "둘 다 거짓"이 축이 된다는 점이 포인트다.

여유가 있다면 한 걸음 더 나가보자. 이 코드가 if (isAdmin || isAdmin)처럼 잘못 짜여 있다면, hasPermission의 독립성을 보이는 쌍을 어떤 조합으로도 만들 수 없다. MC/DC를 만족시키려는 시도가 실패하는 것 자체가 결함 신고가 되는 사례다.

과제 4 — 커버리지 100%인데 버그를 통과시키는 테스트 짓기 (15분)

아래 함수에는 버그가 있다. 구문·분기 커버리지를 100%로 만들면서도 이 버그를 잡지 못하는 테스트 두 개 를 직접 써본다. 그다음, 같은 커버리지를 유지하면서 버그를 잡는 테스트로 고쳐본다.

function applyCoupon(price, coupon) {
  if (coupon.type === 'FIXED') {
    return price + coupon.amount;   // 의도: price - coupon.amount
  }
  return price * (1 - coupon.rate);
}
해설 보기

버그를 못 잡는 테스트의 예시다.

expect(applyCoupon(10000, { type: 'FIXED', amount: 1000 })).toBeGreaterThan(0);
expect(applyCoupon(10000, { type: 'RATE', rate: 0.1 })).toBeLessThan(10000);

두 줄로 양쪽 분기가 다 실행되어 커버리지는 100%다. 하지만 첫 어서션은 11,000도 통과시킨다. 고치면 이렇게 된다.

expect(applyCoupon(10000, { type: 'FIXED', amount: 1000 })).toBe(9000);
expect(applyCoupon(10000, { type: 'RATE', rate: 0.1 })).toBe(9000);

커버리지 숫자는 두 경우가 완전히 같다. 달라진 것은 어서션의 정확도뿐이다. 이 과제에서 얻어야 할 문장은 하나다 — "커버리지는 테스트가 어디를 지나갔는지 말해줄 뿐, 무엇을 확인했는지는 말해주지 않는다."

한 걸음 더: 이 함수에는 커버리지로 절대 드러나지 않는 문제도 있다. coupon.rate가 없는 경우, 할인액이 원래 가격보다 큰 경우, type이 둘 중 어느 것도 아닌 경우다. 코드가 다루지 않는 입력 을 찾는 것은 여전히 사람의 몫이다.

과제 5 — 커버리지 리포트 읽고 질문 만들기 (12분)

아래 리포트를 보고 ① 어느 파일부터 볼 것인지 ② 개발자에게 던질 질문 두 개를 적는다. 숫자를 지적하는 문장이 아니라, 미커버 구간이 무엇인지 묻는 문장 으로 쓴다.

File              | % Stmts | % Branch | Uncovered Line #s
------------------|---------|----------|-------------------
All files         |   78.5  |   64.0   |
 checkout.js      |   91.2  |   88.0   | 120
 refund.js        |   35.0  |   12.0   | 22-58, 71-90
 formatDate.js    |  100.0  |  100.0   |
해설 보기

① refund.js다. 총계 78.5는 formatDate.js의 100%가 끌어올린 숫자이고, 환불은 틀렸을 때 돈이 잘못 나가는 영역 이다. 구문 35% 대비 분기 12%라는 격차도 신호다 — 정상 경로조차 덜 밟혔고 예외 경로는 거의 안 밟혔다는 뜻이다.

② 질문 예시.

  • "refund.js 22~58줄이 부분 환불 처리 같은데, 이 경로는 어떻게 검증하고 계신가요? 단위 테스트로 잡기 어렵다면 제가 수동 케이스로 커버할 목록을 만들어볼까요?"
  • "분기 커버리지가 12%라 실패·예외 경로가 대부분 비어 있는 것 같습니다. 이번 릴리즈 범위에 환불이 포함되는지 먼저 확인하고 싶습니다."

두 질문 모두 비난이 아니라 역할 분담 제안 으로 끝난다는 점이 중요하다. 커버리지 리포트를 QA가 읽는 목적은 개발자를 평가하는 게 아니라, 자동화가 못 닿는 곳을 찾아 수동 테스트 범위를 정하는 것이다.

과제 6 — 커버리지 함정 설명하기 (8분)

"이 코드는 구문 커버리지 100%이니 안심하고 배포해도 된다"는 동료의 말에 어떻게 반박할지, 이 레슨에서 배운 내용을 근거로 3~4문장으로 적는다. 과제 4에서 만든 예시를 근거로 들면 훨씬 설득력이 있다.

자가 체크리스트

  • 구문·분기·조건 커버리지의 차이를 하나의 코드 예시로 각각 계산할 수 있다
  • 조건 커버리지 100%인데 분기 커버리지가 100%가 아닐 수 있음을 예로 보일 수 있다
  • if에 else가 없어도 분기가 두 개로 세어지는 이유를 설명할 수 있다
  • 단축 평가(short-circuit)가 커버리지와 재현성에 어떤 영향을 주는지 설명할 수 있다
  • MC/DC의 정의("각 조건이 독립적으로 결과를 바꾼다")를 케이스 쌍으로 보일 수 있다
  • 조건이 n개일 때 MC/DC 최소 케이스가 보통 n+1개인 이유를 설명할 수 있다
  • 안전 필수 산업이 MC/DC를 요구하는 이유와, 일반 서비스가 안 하는 이유를 비용 관점에서 설명할 수 있다
  • 커버리지 100%인데 결함이 통과하는 코드 예시를 직접 만들 수 있다
  • 커버리지가 구조적으로 못 잡는 결함 유형을 세 가지 이상 댈 수 있다
  • Istanbul/JaCoCo 리포트에서 총계가 아니라 무엇을 봐야 하는지 말할 수 있다
  • "커버리지 80%"라는 숫자를 그대로 믿으면 안 되는 이유를 두 가지 이상 댈 수 있다
  • 커버리지 리포트를 근거로 수동 테스트 범위를 정하는 흐름을 설명할 수 있다
  • 구조 기반 기법이 명세 기반 기법으로 못 찾는 무엇을 찾아주는지 설명할 수 있다

흔한 실수

  • 커버리지 %를 품질의 척도로 오해한다. 커버리지는 "실행 여부"만 잰다. "검증이 제대로 됐는지"는 어서션의 품질에 달려 있다 — 어서션 없는 테스트도 커버리지 100%를 만든다.
  • 분기 커버리지와 조건 커버리지를 같은 것으로 착각한다. A && B 형태에서는 분기 100%여도 개별 조건이 참·거짓을 다 안 겪었을 수 있고, 반대로 조건 100%인데 분기가 50%인 경우도 있다. 후자를 아는 사람이 훨씬 적다.
  • "더 엄격한 기준은 약한 기준을 항상 포함한다"고 가정한다. 조건 커버리지는 분기 커버리지를 포함하지 않는다. 그래서 실무 기준은 조건/판정 커버리지다.
  • 모든 프로젝트에 MC/DC 수준을 요구한다. MC/DC의 진짜 비용은 케이스를 만드는 게 아니라 요구사항이 바뀔 때마다 유지보수하는 것 이다. 안전 필수 영역이 아니라면 분기 커버리지를 기본선으로 두고 위험한 모듈만 올린다.
  • 총계 퍼센트만 보고 판단한다. 테스트하기 쉬운 유틸 파일이 100%를 채우는 동안 결제 모듈이 40%일 수 있다. 파일별 편차와 분기 열, 미커버 라인 번호를 본다.
  • 커버리지를 팀 KPI로 걸어놓고 방치한다. 지표가 목표가 되면 어서션 없는 테스트가 늘어난다. 모듈 2에서 본 대리 지표의 함정이 그대로 반복된다.
  • 미커버 구간을 발견하고도 개발자에게만 넘긴다. 단위 테스트가 못 닿는 구간은 QA의 수동·탐색적 테스트가 커버해야 할 목록 이기도 하다.
  • 커버리지 리포트를 개발자 평가 도구로 쓴다. 그 순간 리포트는 정직하지 않게 된다. 목적은 평가가 아니라 공백의 위치 파악이다.
  • 누락된 요구사항을 커버리지로 잡으려 한다. 구현되지 않은 기능은 코드가 없으므로 분모에도 들어가지 않는다. 이건 요구사항 추적성이 잡는 영역이다.

참고 자료

  • ISTQB Foundation Level Syllabus — Structure-Based Techniques — 구문/분기 커버리지의 공식 정의
  • ISO/IEC/IEEE 29119-4 — MC/DC를 포함한 구조 기반 기법의 표준 정의 (모듈 8에서 자세히)
  • Kelly J. Hayhurst 외, A Practical Tutorial on Modified Condition/Decision Coverage (NASA/TM-2001-210876) — MC/DC를 케이스 쌍 고르는 절차까지 풀어 쓴 무료 문서. 이 레슨의 MC/DC 절이 이 자료의 접근을 따랐다
  • Istanbul 문서 — JavaScript 커버리지 리포트의 각 열(% Stmts, % Branch)이 무엇을 세는지
  • JaCoCo 문서 — Coverage Counters — Java 진영의 커버리지 지표 정의
  • Martin Fowler, "Test Coverage" — 커버리지를 목표 숫자로 삼으면 안 되는 이유를 짧게 정리한 글

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
MC/DC — 변형 조건·결정 커버리지
조건이 여러 개 붙은 판단문에서, 각 조건이 혼자서 결과를 바꾸는 경우까지 모두 확인했는지 재는 기준.
DO-178C — 항공 소프트웨어 인증 지침
비행기에 들어가는 소프트웨어를 어떻게 만들고 증명해야 하는지 정한 문서.
ISO — 국제표준화기구
여러 나라가 모여 "이건 이렇게 하자"를 문서로 정하는 단체. 그 문서 자체를 번호로 부른다.
ASIL — 자동차 안전 완전성 수준
자동차 분야의 위험 등급. A부터 D까지이고 D가 가장 엄격하다.
IEC — 국제전기기술위원회
전기·전자 분야의 국제 규격을 만드는 단체.
FDA — 미국 식품의약국
미국에서 식품·의약품·의료기기를 허가하는 기관.
EN — 유럽 표준
유럽 공통 표준 문서에 붙는 번호 앞머리.
SIL — 안전 완전성 수준 · 소프트웨어 포함 시험
이 커리큘럼에서 두 가지로 쓰인다. ① 고장이 났을 때 사람이 다칠 위험이 큰 정도에 따라 매기는 안전 등급(숫자가 클수록 엄격하다) ② 설계 모델이 아니라 실제로 만들어진 코드를 개발용 컴퓨터에서 돌려 보는 시험 단계. 앞뒤 문장에 등급 숫자가 있으면 ①, 모델·실물 시험과 나란히 나오면 ②다.
PR — 풀 리퀘스트
"내가 고친 코드를 본체에 합쳐 달라"고 올리는 요청. 합치기 전에 사람들이 보고 의견을 단다.
CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.
KPI — 핵심 성과 지표
"잘되고 있는가" 를 대표로 보여 주는 몇 개의 숫자.

이 레슨에 나온 나머지 4개를 포함해 용어집 전체 보기