본문 바로가기

테스트 커버리지와 품질 관리

라인·함수·분기·문장 커버리지의 차이를 이해하고 Jest 임계값과 핵심 시나리오로 테스트 품질을 관리합니다.

앞의 두 글에서 단위 테스트와 E2E 테스트를 작성했습니다. 이 글에서는 테스트 커버리지(test coverage)가 무엇을 뜻하는지, 그리고 이를 코드 품질 관리에 어떻게 활용하는지 다룹니다.

테스트 커버리지는 테스트를 몇 개 작성했는지가 아니라, 작성한 테스트가 코드 경로를 얼마나 많이 실행하는지를 나타내는 지표입니다. 커버리지는 아직 실행해 보지 않은 코드를 찾고 변경의 위험을 드러내는 데 도움을 줍니다. 다만 수치가 높다는 사실만으로 결함이 적거나 동작이 정확하다고 결론 내릴 수는 없습니다.


테스트 커버리지란

테스트 커버리지는 테스트가 소스 코드의 어느 부분을 실행했는지 측정하는 지표입니다. 보통 백분율로 나타내며, 테스트 스위트가 애플리케이션 코드의 얼마만큼을 실행하는지 보여 줍니다. Jest는 다음 네 가지 지표를 보여 줍니다.

  • 라인 커버리지(line coverage): 실행할 수 있는 소스 줄 가운데 테스트가 실행한 줄의 비율입니다. 가장 흔하고 직관적인 지표입니다.
  • 함수 커버리지(function coverage): 전체 함수와 메서드 가운데 테스트가 호출한 것의 비율입니다.
  • 분기 커버리지(branch coverage): if, 조건식, switch 같은 조건의 각 분기 결과를 테스트가 실행했는지 측정합니다. 각 분기를 한 번씩 실행했다고 해서 모든 경로의 조합과 경곗값을 확인했다는 뜻은 아닙니다.
  • 문장 커버리지(statement coverage): 모든 문장(statement)이 한 번 이상 실행되었는지 측정합니다. 라인 커버리지와 비슷하지만, 한 줄에 문장이 여러 개 있으면 따로 셉니다.

커버리지를 높게 유지하면 다음과 같은 이점이 있습니다.

  • 빠진 경로 발견: 실행해 보지 않은 실패·예외·경곗값 경로를 찾아 테스트 후보로 삼을 수 있습니다.
  • 변경 확인: 리팩터링이나 기능 추가 때 관련 경로를 실행하는 테스트가 있으면 회귀를 더 빨리 알아챌 수 있습니다.
  • 문서 역할: 테스트 코드는 실제 코드의 사용 예시를 보여 주므로 일종의 문서가 됩니다.

커버리지 100%가 항상 좋은 것은 아닙니다

100% 커버리지는 계측된 항목을 모두 실행했다는 뜻일 뿐, 요구사항과 단언(assertion)이 완전하다는 뜻은 아닙니다. 모든 상황에서 그만한 비용을 들일 가치가 있는 것도 아닙니다.

  • 비용과 효율: 100%를 채우려고 비즈니스 가치가 적은 코드까지 테스트하면 시간과 자원을 낭비할 수 있습니다.
  • 테스트 품질: 커버리지 숫자가 높다고 테스트 품질이 높은 것은 아닙니다. 코드를 실행만 하고 올바른 시나리오나 경곗값 사례를 확인하지 않는 테스트일 수도 있습니다.
  • 환경 문제: 실제 인프라에서만 드러나는 실패는 통합 테스트, E2E 테스트, sandbox 확인이 필요할 수 있습니다. 정말 도달할 수 없는 코드라면 커버리지에서 제외해 숨기기보다 지우는 것을 먼저 검토합니다.

테스트 커버리지는 코드 품질을 보여 주는 지표 가운데 하나일 뿐, 그 자체가 목표가 되어서는 안 됩니다. 팀 상황과 프로젝트의 중요도에 맞춰 커버리지 목표를 정하고, 숫자와 테스트의 질을 함께 봐야 합니다. 실패했을 때 비용이 크고 자주 바뀌는 핵심 기능에는 더 엄격한 기준을, 위험이 낮은 단순한 코드에는 비용에 맞는 기준을 적용할 수 있습니다. 다만 코드의 종류만 보고 일률적으로 낮은 기준을 주지는 않습니다.


NestJS에서 테스트 커버리지 측정

Jest는 커버리지 리포트 기능을 자체적으로 제공합니다. 1장에서 CommonJS로 만든 NestJS 12 프로젝트의 package.json에는 이미 다음 테스트 스크립트가 들어 있습니다.

package.json (부분 발췌)
{
  "scripts": {
    "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
    "test:cov": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage",
    "test:debug": "node --inspect-brk --experimental-vm-modules -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json"
  }
}

모든 스크립트가 같은 Node.js 옵션으로 Jest를 실행하며, 커버리지 측정에는 --coverage를 붙인 test:cov를 씁니다. test:cov는 --config를 지정하지 않으므로 루트의 jest.config.ts를 읽고, 그 설정의 testRegex가 고르는 단위 테스트만 실행합니다. E2E 테스트의 커버리지가 필요하다면 test:e2e에 --coverage를 붙인 별도 스크립트를 만듭니다.

커버리지 리포트를 만들려면 다음 명령을 실행합니다.

npm run test:cov

Jest가 테스트를 실행한 뒤 콘솔에 다음과 같은 요약 표를 출력합니다. 아래 표는 출력 형식을 보여 주기 위한 예시이며, 실제 숫자와 파일 목록은 프로젝트마다 다릅니다.

---------------------------|---------|----------|---------|---------|-------------------
File                       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------|---------|----------|---------|---------|-------------------
All files                  |   88.88 |      100 |   77.77 |   88.88 |
 app.controller.ts         |     100 |      100 |     100 |     100 |
 app.service.ts            |     100 |      100 |     100 |     100 |
 users/users.controller.ts |   71.42 |      100 |      50 |   71.42 | 19,20
 users/users.service.ts    |   85.71 |      100 |      50 |   85.71 | 22-26
---------------------------|---------|----------|---------|---------|-------------------

이 표는 파일마다 문장, 분기, 함수, 라인 커버리지를 보여 주고, 실행되지 않은 줄 번호(Uncovered Line #s)도 알려 줍니다.

맨 위의 All files는 파일별 백분율을 평균 낸 값이 아니라, 계측된 전체 항목의 실행 수와 전체 수를 합쳐 계산한 값입니다. 라인만 예로 들어 보겠습니다. 작은 파일 A는 실행할 수 있는 1줄 가운데 1줄을, 파일 B는 9줄 가운데 1줄을 실행했습니다. A는 100%, B는 약 11.11%이지만 전체는 (1 + 1) ÷ (1 + 9) × 100 = 20%입니다. 두 비율의 단순 평균인 약 55.56%를 전체 값으로 쓰면 큰 파일에서 실행되지 않은 줄을 과소평가하게 됩니다.

명령을 실행하면 jest.config.ts의 coverageDirectory에 지정된 coverage 폴더가 프로젝트 루트에 생기고, 그 안에 자세한 HTML 리포트가 들어 있습니다. coverage/lcov-report/index.html을 브라우저로 열면 어떤 줄이 테스트되지 않았는지 색으로 확인할 수 있어 테스트를 보강하는 데 도움이 됩니다.

Jest 커버리지를 위험 경로로 읽는 법

같은 설정 계측·미실행 위치·업무 위험·입력/기대 테스트·재실행에서 근거와 단언이 누적된다.

Jest의 statement, branch, function, line 커버리지가 각각 측정하는 실행 사실과 증명하지 못하는 품질을 구분하고, 리포트의 누락 위치를 위험 시나리오와 assertion으로 바꾸는 해석 흐름

Nest · Coverage evidence

커버리지에서 테스트 위험 읽기

커버리지는 실행한 위치를 보여 줍니다. 빠진 줄과 분기를 업무 위험에 연결하고, 결과를 검증하는 assertion으로 보강합니다.

Jest 커버리지 지표가 답하는 질문과 한계
지표측정하는 실행 사실그 자체로 증명하지 못하는 것
statements계측된 실행 문장이 한 번 이상 실행됐는가결과를 올바르게 assertion했는가
branches계측된 조건의 각 분기 결과를 실행했는가모든 경로 조합과 경계값을 검증했는가
functions함수나 메서드가 한 번 이상 호출됐는가입력 영역과 부작용을 충분히 검사했는가
lines실행 가능한 소스 라인을 지나갔는가같은 줄에 있는 모든 논리를 검증했는가
퍼센트 뒤의 위험 경로와 기대 결과를 찾는다 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다. 남은 위험 동일 범위 계측 test:cov · 설정 확인 누락 위치 확인 파일 · 라인 · 분기 업무 위험 해석 실패 · 권한 · 빈 값 입력과 결과 설계 관찰 가능한 단언 테스트 추가 · 실행 위험 경로를 재현 숫자와 단언 확인 남은 위험 재검토
계측 위치를 위험과 테스트로 연결한다 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다. 동일 범위 계측 test:cov · 설정 확인 누락 위치 확인 파일 · 라인 · 분기 업무 위험 해석 실패 · 권한 · 빈 값 입력과 결과 설계 관찰 가능한 assertion 테스트 추가 · 실행 위험 경로를 재현 숫자와 단언 확인 남은 위험 재검토
Statements

실행 문장을 계측한다

측정하는 실행 사실
계측된 실행 문장이 한 번 이상 실행됐는가
그 자체로 증명하지 못하는 것
결과를 올바르게 assertion했는가
Branches

조건 결과를 나눠 본다

측정하는 실행 사실
계측된 조건의 각 분기 결과를 실행했는가
그 자체로 증명하지 못하는 것
모든 경로 조합과 경계값을 검증했는가
Functions

호출 여부를 센다

측정하는 실행 사실
함수나 메서드가 한 번 이상 호출됐는가
그 자체로 증명하지 못하는 것
입력 영역과 부작용을 충분히 검사했는가
Lines

실행 가능한 줄을 센다

측정하는 실행 사실
실행 가능한 소스 라인을 지나갔는가
그 자체로 증명하지 못하는 것
같은 줄에 있는 모든 논리를 검증했는가
Report → test

누락 위치를 의미 있는 시나리오로 바꾼다

  1. npm run test:cov로 같은 테스트 범위를 실행한다

    설정과 대상 파일이 같은지 먼저 고정합니다.

  2. 전체 집계에서 낮은 파일·지표를 찾는다

    HTML 리포트와 Uncovered Line #s를 함께 봅니다.

  3. 빠진 줄·분기가 나타내는 업무 위험을 적는다

    실패, 예외, 빈 값, 권한, 경계 조건 중 무엇이 비었는지 해석합니다.

  4. 입력과 기대 결과를 가진 테스트를 추가한다

    코드를 통과시키는 호출이 아니라 관찰 가능한 결과를 검증합니다.

  5. 리포트와 테스트 실패 신호를 함께 재확인한다

    숫자가 올라가도 결함을 잡지 못하면 시나리오를 다시 설계합니다.

Not proof

100%도 정확성 증명은 아니다

실행된 코드가 assertion 없이 지나가거나 잘못된 요구사항을 검증할 수 있습니다.

Useful signal

변경 위험과 함께 읽는다

실패 비용과 변경 빈도가 큰 경계부터 누락된 테스트를 보강합니다.

커버리지의 질문은 “몇 퍼센트인가”에서 끝나지 않습니다. “어떤 위험 경로가 비었고, 어떤 assertion이 그 동작을 지킬 것인가”까지 답해야 품질 신호가 됩니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Jest 커버리지를 위험 경로로 읽는 법
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
같은 설정 계측

coverage의 미실행 line·branch 위치를 봅니다.

근거: 같은 설정 · 자료: 미실행 위치수치를 위험 분석의 입력으로 삼습니다.
위험·기대 테스트

실패·빈 값·권한 위험을 검사할 입력·기대 결과를 작성합니다.

산출물: 위험 시나리오·단언미실행 위치가 의미 있는 시험으로 바뀝니다.
재실행 확인

다시 실행해 의도한 오류를 단언이 잡는지 봅니다.

검증: 단언 검출 · 수치 상승: 정확성 증명 아님coverage와 행동 검증을 함께 확인합니다.

Jest 커버리지를 위험 경로로 읽는 법

같은 설정 계측·미실행 위치·업무 위험·입력/기대 테스트·재실행에서 근거와 단언이 누적된다.

Jest의 statement, branch, function, line 커버리지가 각각 측정하는 실행 사실과 증명하지 못하는 품질을 구분하고, 리포트의 누락 위치를 위험 시나리오와 assertion으로 바꾸는 해석 흐름

Nest · Coverage evidence

커버리지에서 테스트 위험 읽기

커버리지는 실행한 위치를 보여 줍니다. 빠진 줄과 분기를 업무 위험에 연결하고, 결과를 검증하는 assertion으로 보강합니다.

Jest 커버리지 지표가 답하는 질문과 한계
지표측정하는 실행 사실그 자체로 증명하지 못하는 것
statements계측된 실행 문장이 한 번 이상 실행됐는가결과를 올바르게 assertion했는가
branches계측된 조건의 각 분기 결과를 실행했는가모든 경로 조합과 경계값을 검증했는가
functions함수나 메서드가 한 번 이상 호출됐는가입력 영역과 부작용을 충분히 검사했는가
lines실행 가능한 소스 라인을 지나갔는가같은 줄에 있는 모든 논리를 검증했는가
퍼센트 뒤의 위험 경로와 기대 결과를 찾는다 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다. 남은 위험 동일 범위 계측 test:cov · 설정 확인 누락 위치 확인 파일 · 라인 · 분기 업무 위험 해석 실패 · 권한 · 빈 값 입력과 결과 설계 관찰 가능한 단언 테스트 추가 · 실행 위험 경로를 재현 숫자와 단언 확인 남은 위험 재검토
계측 위치를 위험과 테스트로 연결한다 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다. 동일 범위 계측 test:cov · 설정 확인 누락 위치 확인 파일 · 라인 · 분기 업무 위험 해석 실패 · 권한 · 빈 값 입력과 결과 설계 관찰 가능한 assertion 테스트 추가 · 실행 위험 경로를 재현 숫자와 단언 확인 남은 위험 재검토
Statements

실행 문장을 계측한다

측정하는 실행 사실
계측된 실행 문장이 한 번 이상 실행됐는가
그 자체로 증명하지 못하는 것
결과를 올바르게 assertion했는가
Branches

조건 결과를 나눠 본다

측정하는 실행 사실
계측된 조건의 각 분기 결과를 실행했는가
그 자체로 증명하지 못하는 것
모든 경로 조합과 경계값을 검증했는가
Functions

호출 여부를 센다

측정하는 실행 사실
함수나 메서드가 한 번 이상 호출됐는가
그 자체로 증명하지 못하는 것
입력 영역과 부작용을 충분히 검사했는가
Lines

실행 가능한 줄을 센다

측정하는 실행 사실
실행 가능한 소스 라인을 지나갔는가
그 자체로 증명하지 못하는 것
같은 줄에 있는 모든 논리를 검증했는가
Report → test

누락 위치를 의미 있는 시나리오로 바꾼다

  1. npm run test:cov로 같은 테스트 범위를 실행한다

    설정과 대상 파일이 같은지 먼저 고정합니다.

  2. 전체 집계에서 낮은 파일·지표를 찾는다

    HTML 리포트와 Uncovered Line #s를 함께 봅니다.

  3. 빠진 줄·분기가 나타내는 업무 위험을 적는다

    실패, 예외, 빈 값, 권한, 경계 조건 중 무엇이 비었는지 해석합니다.

  4. 입력과 기대 결과를 가진 테스트를 추가한다

    코드를 통과시키는 호출이 아니라 관찰 가능한 결과를 검증합니다.

  5. 리포트와 테스트 실패 신호를 함께 재확인한다

    숫자가 올라가도 결함을 잡지 못하면 시나리오를 다시 설계합니다.

Not proof

100%도 정확성 증명은 아니다

실행된 코드가 assertion 없이 지나가거나 잘못된 요구사항을 검증할 수 있습니다.

Useful signal

변경 위험과 함께 읽는다

실패 비용과 변경 빈도가 큰 경계부터 누락된 테스트를 보강합니다.

커버리지의 질문은 “몇 퍼센트인가”에서 끝나지 않습니다. “어떤 위험 경로가 비었고, 어떤 assertion이 그 동작을 지킬 것인가”까지 답해야 품질 신호가 됩니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 같은 설정으로 커버리지를 계측한 뒤 미실행 라인과 분기 위치를 확인한다. 위치를 실패·빈 값·권한 같은 업무 위험으로 해석하고 입력과 기대 결과가 있는 테스트를 추가한다. 다시 실행하여 숫자뿐 아니라 의도한 오류를 단언이 잡는지 확인한다. 마지막 점선은 남은 위험을 재검토하는 반복이며 수치 상승이 정확성을 증명한다는 뜻이 아니다. 네 지표의 의미와 한계 비교는 아래에 유지된다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Jest 커버리지를 위험 경로로 읽는 법
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
같은 설정 계측

coverage의 미실행 line·branch 위치를 봅니다.

근거: 같은 설정 · 자료: 미실행 위치수치를 위험 분석의 입력으로 삼습니다.
위험·기대 테스트

실패·빈 값·권한 위험을 검사할 입력·기대 결과를 작성합니다.

산출물: 위험 시나리오·단언미실행 위치가 의미 있는 시험으로 바뀝니다.
재실행 확인

다시 실행해 의도한 오류를 단언이 잡는지 봅니다.

검증: 단언 검출 · 수치 상승: 정확성 증명 아님coverage와 행동 검증을 함께 확인합니다.

위 그림의 위쪽 표는 네 가지 지표가 각각 무엇을 측정하고, 그 자체로는 무엇을 증명하지 못하는지 정리합니다. 예를 들어 분기 커버리지는 각 조건의 결과를 실행했는지는 알려 주지만, 모든 경로 조합과 경곗값을 확인했는지는 알려 주지 않습니다. 아래쪽 흐름은 리포트를 읽는 순서입니다. 같은 설정으로 측정한 뒤 실행되지 않은 줄과 분기를 찾고, 그것이 실패·빈 값·권한 같은 어떤 업무 위험인지 해석해 입력과 기대 결과를 가진 테스트를 추가한 다음, 다시 실행해 숫자뿐 아니라 의도한 오류를 단언이 잡아내는지 확인합니다.


커버리지 임계값과 품질 관리

커버리지를 품질 관리에 쓰려면 특정 수준을 강제하는 임계값(threshold)을 정할 수 있습니다. CI/CD 파이프라인에서 커버리지가 기준에 못 미치면 빌드를 실패시키는 방식으로 활용합니다.

test:cov가 실제로 읽는 jest.config.ts에 coverageThreshold를 추가합니다. 아래 코드는 NestJS 12 템플릿이 만든 설정에 임계값만 더한 것입니다. 별도의 E2E 설정은 test:cov가 그 파일을 지정할 때만 이 실행에 적용됩니다.

jest.config.ts
import type { Config } from 'jest';
import { pathsToModuleNameMapper } from 'ts-jest';
import ts from 'typescript';

// 경로 별칭은 tsconfig.json에서 읽어 옵니다.
const { config: tsconfig } = ts.readConfigFile(
  './tsconfig.json',
  ts.sys.readFile,
);
const paths = tsconfig?.compilerOptions?.paths ?? {};

const config: Config = {
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: '.',
  testRegex: '.*\\.spec\\.ts$', // 단위 테스트 파일만 선택
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: '<rootDir>/' }),
  collectCoverageFrom: [
    'src/**/*.(t|j)s',
    'libs/**/*.(t|j)s',
    'apps/**/*.(t|j)s',
  ],
  coverageDirectory: './coverage',
  testEnvironment: 'node',
  // 설명을 위한 예시 값입니다.
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    './src/users/': {
      lines: 80,
      functions: 50,
    },
    './src/app*.ts': {
      lines: 95,
    },
  },
};

export default config;

커버리지 임계값을 CI 품질 루프로 운영하는 법

같은 임계값 계측·미달/통과 판정·위험 시나리오 보완·재계측의 상태이며 통과를 완전한 정확성으로 보지 않는다.

Jest coverageThreshold의 global, 디렉터리 집계, glob별 파일 판정과 Jest 30.5.1의 모든 파일 매치 시 global fallback, 양수 백분율과 음수 미커버 개수 의미를 구분하고 위험 기반 테스트 보강을 반복하는 운영 루프

Nest · Coverage quality gate

커버리지 최소선과 리뷰의 역할

Jest 임계값은 커버리지 후퇴를 막는 최소선입니다. 통과 뒤에도 빠진 경로와 업무 위험을 검토합니다. 아래 숫자는 본문의 설정 예시입니다.

Jest config

전체 기준과 위험 경로 기준을 분리한다

coverageThreshold: {
  global: { branches: 80, lines: 80 },
  './src/users/': { lines: 80 },
  './src/app*.ts': { lines: 95 },
}

디렉터리는 하위 파일을 묶어 집계하고 glob은 각 매치 파일을 판정합니다. 특정 그룹은 나머지 global 집계에서 빠지며, 남은 파일이 없으면 Jest 30.5.1의 global은 전체 계측 파일로 fallback합니다. Jest 30.2.0에는 이 fallback이 없습니다.

Threshold semantics

부호가 기준 단위를 바꾼다

  • 양수 80: 커버된 비율이 80% 이상이어야 통과
  • 음수 -10: 미커버 항목이 최대 10개여야 통과
  • statement, branch, function, line을 서로 독립적으로 판정
  • 경로·glob이 아무 파일도 찾지 못하면 설정 오류
  • 대상 수가 작은 파일은 백분율 변화 폭이 크므로 맥락을 함께 기록
지표별 gate와 코드 리뷰 질문
지표CI가 막는 후퇴리뷰에서 이어 물을 질문
branches조건 결과가 테스트에서 사라짐실패·예외·권한·빈 값 분기가 실제로 검증되는가
functions새 함수가 전혀 호출되지 않음공개 계약과 부작용에 assertion이 있는가
lines핵심 줄이 실행 범위 밖에 남음누락 줄이 죽은 코드인지 빠진 시나리오인지 구분했는가
statements실행 문장 범위가 낮아짐통과를 위한 호출만 추가해 지표를 속이지 않았는가
gate 통과와 테스트 위험 검토를 이어간다 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다. 미달 통과 보강 필요 재계측 대상과 최소선 같은 Jest 설정 테스트 · 계측 covered / total 임계값 판정 그룹 · 지표별 gate 미달 변경 중단 · 누락 확인 통과 최소선 · 위험 리뷰 시나리오 보강 입력 · 결과 · 실패 동일 설정 재검사 남은 위험도 기록
최소선 판정과 위험 검토의 분기 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다. 미달 통과 대상과 최소선 같은 Jest 설정 테스트 · 계측 covered / total 임계값 판정 그룹 · 지표별 gate 미달 변경 중단 누락 확인 CI 실패 통과 최소선 충족 업무 위험 리뷰 계속 시나리오 보강 입력 · 결과 · 실패 동일 설정 재검사 남은 위험도 기록
Branches

실패 경로를 지킨다

CI가 막는 후퇴
조건 결과가 테스트에서 사라짐
리뷰에서 이어 물을 질문
실패·예외·권한·빈 값 분기가 실제로 검증되는가
Functions

호출 뒤 계약을 본다

CI가 막는 후퇴
새 함수가 전혀 호출되지 않음
리뷰에서 이어 물을 질문
공개 계약과 부작용에 assertion이 있는가
Lines

누락 이유를 분류한다

CI가 막는 후퇴
핵심 줄이 실행 범위 밖에 남음
리뷰에서 이어 물을 질문
누락 줄이 죽은 코드인지 빠진 시나리오인지 구분했는가
Statements

지표용 호출을 경계한다

CI가 막는 후퇴
실행 문장 범위가 낮아짐
리뷰에서 이어 물을 질문
통과를 위한 호출만 추가해 지표를 속이지 않았는가
Operating loop

최소선과 위험 검토를 같은 루프로 묶는다

  1. 팀 최소선과 핵심 경로 기준을 합의한다

  2. CI에서 같은 설정으로 테스트와 계측을 실행한다

  3. 미달 변경을 멈추고 누락 위치를 읽는다

  4. 고위험 경계부터 테스트를 보강한다

  5. 결과와 남은 위험을 근거로 기준을 재검토한다

임계값 통과는 검토의 끝이 아닙니다. gate가 후퇴를 멈추면 리포트가 빠진 경로를 가리키고, 리뷰가 그 경로의 테스트 가치와 다음 행동을 결정합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1커버리지 임계값을 CI 품질 루프로 운영하는 법
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
같은 threshold 판정

같은 대상·threshold로 계측합니다.

미달: 변경 중지 · 통과: 최소선gate가 현재 변경의 다음 행동을 정합니다.
누락·위험 보완

미달이면 누락된 test를, 통과해도 업무 위험을 검토합니다.

두 경로: 기대 시나리오 보완 · 통과: 완전 검증 아님숫자 뒤에 남은 위험을 확인합니다.
같은 설정 재계측

보완한 시나리오를 동일 설정으로 다시 실행합니다.

설정: 동일 · 결과: 새 계측 근거변경 전후의 비교 기준을 유지합니다.

커버리지 임계값을 CI 품질 루프로 운영하는 법

같은 임계값 계측·미달/통과 판정·위험 시나리오 보완·재계측의 상태이며 통과를 완전한 정확성으로 보지 않는다.

Jest coverageThreshold의 global, 디렉터리 집계, glob별 파일 판정과 Jest 30.5.1의 모든 파일 매치 시 global fallback, 양수 백분율과 음수 미커버 개수 의미를 구분하고 위험 기반 테스트 보강을 반복하는 운영 루프

Nest · Coverage quality gate

커버리지 최소선과 리뷰의 역할

Jest 임계값은 커버리지 후퇴를 막는 최소선입니다. 통과 뒤에도 빠진 경로와 업무 위험을 검토합니다. 아래 숫자는 본문의 설정 예시입니다.

Jest config

전체 기준과 위험 경로 기준을 분리한다

coverageThreshold: {
  global: { branches: 80, lines: 80 },
  './src/users/': { lines: 80 },
  './src/app*.ts': { lines: 95 },
}

디렉터리는 하위 파일을 묶어 집계하고 glob은 각 매치 파일을 판정합니다. 특정 그룹은 나머지 global 집계에서 빠지며, 남은 파일이 없으면 Jest 30.5.1의 global은 전체 계측 파일로 fallback합니다. Jest 30.2.0에는 이 fallback이 없습니다.

Threshold semantics

부호가 기준 단위를 바꾼다

  • 양수 80: 커버된 비율이 80% 이상이어야 통과
  • 음수 -10: 미커버 항목이 최대 10개여야 통과
  • statement, branch, function, line을 서로 독립적으로 판정
  • 경로·glob이 아무 파일도 찾지 못하면 설정 오류
  • 대상 수가 작은 파일은 백분율 변화 폭이 크므로 맥락을 함께 기록
지표별 gate와 코드 리뷰 질문
지표CI가 막는 후퇴리뷰에서 이어 물을 질문
branches조건 결과가 테스트에서 사라짐실패·예외·권한·빈 값 분기가 실제로 검증되는가
functions새 함수가 전혀 호출되지 않음공개 계약과 부작용에 assertion이 있는가
lines핵심 줄이 실행 범위 밖에 남음누락 줄이 죽은 코드인지 빠진 시나리오인지 구분했는가
statements실행 문장 범위가 낮아짐통과를 위한 호출만 추가해 지표를 속이지 않았는가
gate 통과와 테스트 위험 검토를 이어간다 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다. 미달 통과 보강 필요 재계측 대상과 최소선 같은 Jest 설정 테스트 · 계측 covered / total 임계값 판정 그룹 · 지표별 gate 미달 변경 중단 · 누락 확인 통과 최소선 · 위험 리뷰 시나리오 보강 입력 · 결과 · 실패 동일 설정 재검사 남은 위험도 기록
최소선 판정과 위험 검토의 분기 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다. 미달 통과 대상과 최소선 같은 Jest 설정 테스트 · 계측 covered / total 임계값 판정 그룹 · 지표별 gate 미달 변경 중단 누락 확인 CI 실패 통과 최소선 충족 업무 위험 리뷰 계속 시나리오 보강 입력 · 결과 · 실패 동일 설정 재검사 남은 위험도 기록
Branches

실패 경로를 지킨다

CI가 막는 후퇴
조건 결과가 테스트에서 사라짐
리뷰에서 이어 물을 질문
실패·예외·권한·빈 값 분기가 실제로 검증되는가
Functions

호출 뒤 계약을 본다

CI가 막는 후퇴
새 함수가 전혀 호출되지 않음
리뷰에서 이어 물을 질문
공개 계약과 부작용에 assertion이 있는가
Lines

누락 이유를 분류한다

CI가 막는 후퇴
핵심 줄이 실행 범위 밖에 남음
리뷰에서 이어 물을 질문
누락 줄이 죽은 코드인지 빠진 시나리오인지 구분했는가
Statements

지표용 호출을 경계한다

CI가 막는 후퇴
실행 문장 범위가 낮아짐
리뷰에서 이어 물을 질문
통과를 위한 호출만 추가해 지표를 속이지 않았는가
Operating loop

최소선과 위험 검토를 같은 루프로 묶는다

  1. 팀 최소선과 핵심 경로 기준을 합의한다

  2. CI에서 같은 설정으로 테스트와 계측을 실행한다

  3. 미달 변경을 멈추고 누락 위치를 읽는다

  4. 고위험 경계부터 테스트를 보강한다

  5. 결과와 남은 위험을 근거로 기준을 재검토한다

임계값 통과는 검토의 끝이 아닙니다. gate가 후퇴를 멈추면 리포트가 빠진 경로를 가리키고, 리뷰가 그 경로의 테스트 가치와 다음 행동을 결정합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 같은 대상과 임계값으로 계측한 결과를 gate에서 판정한다. 미달은 변경을 멈추고 누락 테스트를 보강하는 경로, 통과는 최소선만 충족한 상태에서 업무 위험을 검토하는 경로다. 두 경로의 다음 행동은 시나리오와 기대 결과를 보완하고 동일 설정으로 재계측하는 것이다. 임계값 통과를 모든 요구사항 검증 완료로 바꾸지 않는다. 경로 집계와 glob 개별 판정 및 global 버전 차이는 아래 기존 설명을 유지한다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1커버리지 임계값을 CI 품질 루프로 운영하는 법
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
같은 threshold 판정

같은 대상·threshold로 계측합니다.

미달: 변경 중지 · 통과: 최소선gate가 현재 변경의 다음 행동을 정합니다.
누락·위험 보완

미달이면 누락된 test를, 통과해도 업무 위험을 검토합니다.

두 경로: 기대 시나리오 보완 · 통과: 완전 검증 아님숫자 뒤에 남은 위험을 확인합니다.
같은 설정 재계측

보완한 시나리오를 동일 설정으로 다시 실행합니다.

설정: 동일 · 결과: 새 계측 근거변경 전후의 비교 기준을 유지합니다.

위 그림은 임계값을 CI의 품질 점검으로 운영하는 방법을 보여 줍니다. 위쪽은 global, 디렉터리, glob 기준이 각각 어떤 범위를 묶어 판정하는지와, 양수와 음수 값의 뜻을 설명합니다. 아래쪽 흐름에서 같은 설정으로 측정한 결과가 기준에 못 미치면 변경을 멈추고 빠진 테스트를 보강하며, 통과하더라도 최소선만 넘었을 뿐이므로 업무 위험을 계속 검토합니다. 임계값을 통과했다는 사실을 모든 요구사항을 확인했다는 뜻으로 바꾸어 읽지 않는 것이 핵심입니다.

coverageThreshold의 각 기준은 다음과 같이 동작합니다.

  • global: 경로나 glob 기준에 해당하지 않는 나머지 파일들을 하나로 합쳐 최소 임계값을 적용합니다. Jest 30.4.0부터는 모든 파일이 특정 기준에 해당해 남은 파일이 없으면 전체 계측 파일을 합친 값으로 판정합니다. Jest 30.3 이하에는 이 동작이 없으므로 설치한 버전을 확인합니다.
  • 디렉터리 경로는 그 아래 파일들을 하나로 합쳐 판정하고, glob은 해당하는 각 파일을 하나씩 판정하며, 파일 경로는 그 파일만 판정합니다. 지정한 경로나 glob이 아무 파일도 찾지 못하면 Jest가 오류를 냅니다.
  • 양수는 최소 커버 비율이고, 음수는 허용하는 최대 미실행 항목 수입니다.
  • global과 경로·glob 기준을 함께 두면 Jest는 지정한 대상을 따로 판정하고, 그 파일들은 나머지 global 집계에서 뺍니다.

위 숫자는 설명을 위한 예시일 뿐 모든 팀에 맞는 정답이 아닙니다. 또 collectCoverageFrom의 glob은 <rootDir>을 기준으로 해석되지만, Jest 30에서 상대 경로로 적은 coverageThreshold 키는 Jest 프로세스의 현재 작업 디렉터리를 기준으로 해석됩니다. 위 예시는 프로젝트 루트에서 Jest를 실행해 현재 작업 디렉터리와 <rootDir>이 같은 경우입니다. 이 동작은 Jest의 coverageThreshold 문서와 설치한 버전의 CoverageReporter 구현을 함께 보며 확인합니다.

임계값을 설정한 뒤 npm run test:cov를 실행했을 때 기준을 채우지 못하면 Jest는 오류를 내고 0이 아닌 종료 코드로 끝납니다. CI 파이프라인은 이를 실패로 처리하므로, 커버리지가 합의한 수준 아래로 떨어지는 것을 막을 수 있습니다.

커버리지는 다음과 같이 품질 관리에 활용합니다.

  • 초기 목표 설정: 프로젝트 초기에 팀과 합의해, 위험과 현재 수준에 맞는 최소선을 정합니다. 숫자는 프로젝트 사정에 따라 정하고, 보편적인 권장값처럼 쓰지 않습니다.
  • CI/CD 연동: 모든 푸시나 풀 리퀘스트에서 파이프라인이 자동으로 커버리지를 측정하고 임계값을 검사하게 합니다. 기준에 못 미치면 빌드를 실패시켜 커버리지가 조용히 떨어지는 것을 막습니다. 다만 이 검사만으로 테스트 품질 전체가 보장되지는 않습니다.
  • 정기적인 확인: 리포트를 주기적으로 보고 커버리지가 낮은 부분을 찾아 개선합니다.
  • 코드 리뷰: 리뷰할 때 리포트를 참고해, 중요한 업무 로직이나 복잡한 부분에 테스트가 충분한지 확인합니다.
  • 테스트 문화: 개발자가 테스트 작성을 습관으로 삼고, 커버리지를 품질을 보여 주는 지표 가운데 하나로 받아들이게 합니다.

정리

  • 커버리지는 테스트가 실행한 코드의 비율이며, Jest는 문장·분기·함수·라인 네 가지 지표를 보여 줍니다.
  • All files는 파일별 백분율의 평균이 아니라 전체 항목의 실행 수와 전체 수를 합쳐 계산합니다.
  • NestJS 12의 CommonJS 프로젝트에서는 npm run test:cov가 jest.config.ts를 읽어 커버리지를 측정하고, coverage 폴더에 HTML 리포트를 만듭니다.
  • coverageThreshold에서 디렉터리 기준은 파일을 합쳐서, glob 기준은 파일마다 판정하며, 양수는 최소 비율, 음수는 최대 미실행 항목 수입니다.
  • 임계값은 커버리지가 떨어지는 것을 막는 최소선일 뿐이며, 빠진 경로가 어떤 업무 위험인지와 단언이 결과를 확인하는지는 따로 검토합니다.

연습 문제

1. 디렉터리 기준과 glob 기준

라인 임계값만 본다고 가정합니다. 한 디렉터리 안의 파일 A는 10줄 가운데 9줄을, 파일 B는 1줄 가운데 0줄을 실행했습니다. 디렉터리 기준의 최소선을 80%로 두면 통과할까요? 같은 두 파일을 glob으로 각각 80% 이상 요구하면 결과가 같을까요? 나머지 global 대상은 5줄 가운데 4줄을 실행했고, 다른 지표와 기준은 모두 충족한다고 가정합니다.

해설 보기

디렉터리 기준은 두 파일을 합쳐 9 ÷ 11 × 100 ≈ 81.82%로 계산하므로 80%를 통과합니다. glob 기준은 파일마다 판정하므로 A의 90%는 통과하지만 B의 0%는 실패합니다. 두 경우 모두 나머지 global 집계는 4 ÷ 5 × 100 = 80%로 통과합니다.

따라서 디렉터리 기준을 통과했다는 사실만으로 그 안의 모든 파일이 80%라고 말할 수는 없습니다. 어떤 범위를 함께 합쳐 판정했는지가 결과를 좌우합니다.

2. 음수 임계값

global에 branches: -10을 설정했습니다. 측정해 보니 계측된 분기 50개 가운데 38개가 실행되었습니다. 이 설정은 통과할까요? 같은 결과에서 branches: 75로 바꾸면 어떻게 될까요?

해설 보기

음수 값은 허용하는 최대 미실행 항목 수입니다. 실행되지 않은 분기는 50 - 38 = 12개로 허용치 10개를 넘으므로 실패합니다.

양수 75는 최소 비율이므로 38 ÷ 50 × 100 = 76%가 75% 이상이어서 통과합니다. 같은 측정 결과라도 기준의 부호에 따라 판정이 달라지므로, 음수 기준은 분기 수가 크게 늘거나 줄 때 의미가 바뀐다는 점까지 고려해 고릅니다.

3. 단언 없는 100%

AppService 테스트를 다음처럼 바꾸었습니다. it('sum', () => { service.sum(1, 2); }); 커버리지 리포트에서 app.service.ts의 라인 커버리지는 어떻게 될까요? 누군가 sum()을 실수로 a - b로 바꾸면 이 테스트는 어떻게 될까요?

해설 보기

sum()의 본문이 실행되므로 해당 줄은 실행된 것으로 계측되고, 다른 메서드도 테스트되었다면 파일의 라인 커버리지는 100%가 될 수 있습니다.

하지만 반환값을 확인하는 expect가 없으므로 sum()이 a - b를 반환해도 테스트는 그대로 통과합니다. 커버리지는 코드가 실행되었다는 사실만 보여 줄 뿐 결과가 맞는지는 알려 주지 않습니다. 그래서 리뷰에서는 숫자와 함께 각 테스트가 관찰 가능한 결과를 단언하는지 확인해야 합니다.

다음 글 지속적 통합(CI)에 테스트 통합에서는 지금까지 작성한 테스트와 커버리지 검사를 GitHub Actions 같은 CI 파이프라인에 연결합니다.