테스트 커버리지와 품질 관리
라인·함수·분기·문장 커버리지의 차이를 이해하고 Jest 임계값과 핵심 시나리오로 테스트 품질을 관리합니다.
이 절에서는 테스트 커버리지(Test Coverage)의 의미와 이를 활용한 코드 품질 관리 방법을 다룹니다.
테스트 커버리지는 단순히 테스트가 얼마나 많이 작성되었는가를 넘어, 작성된 테스트가 얼마나 많은 코드 경로를 실행하는가를 나타내는 중요한 지표입니다.
커버리지는 아직 실행해 보지 않은 코드를 찾고 변경 위험을 드러내는 데 도움을 줍니다. 다만 높은 수치만으로 결함이 적거나 동작이 정확하다고 결론 내릴 수는 없습니다.
테스트 커버리지란?
테스트 커버리지(Test Coverage)는 소프트웨어 테스트가 소스 코드의 특정 부분을 실행하는 정도를 측정하는 지표입니다.
일반적으로 백분율(%)로 표시되며, 테스트 스위트가 애플리케이션 코드의 얼마나 많은 부분을 실행하는지 보여줍니다.
테스트 커버리지의 주요 유형- 라인 커버리지(Line Coverage): 소스 코드의 전체 실행 가능한 라인 중 몇 라인이 테스트에 의해 실행되었는지 측정합니다. 가장 일반적이고 직관적인 지표입니다.
- 함수/메서드 커버리지(Function/Method Coverage): 전체 함수/메서드 중 몇 개가 테스트에 의해 호출되었는지 측정합니다.
- 브랜치 커버리지(Branch Coverage):
if, 조건식,switch등 계측된 조건의 각 분기 결과가 테스트에 의해 실행되었는지 측정합니다. 각 분기를 실행했다는 사실이 모든 경로 조합과 경계값을 검증했다는 뜻은 아닙니다. - 문장 커버리지(Statement Coverage): 모든 문장(statement)이 한 번 이상 실행되었는지 측정합니다. 라인 커버리지와 유사하지만, 공백이나 주석은 제외됩니다.
- 누락 경로 발견: 실행하지 않은 실패·예외·경계 경로를 찾아 테스트 후보로 바꿀 수 있습니다.
- 코드 변경의 관찰성: 리팩토링이나 기능 추가 시, 관련 경로를 실행하는 테스트가 있다면 회귀 신호를 더 빨리 얻을 수 있습니다.
- 문서화 역할: 테스트 코드는 실제 코드의 사용 예시를 보여주므로, 일종의 문서화 역할을 합니다.
- 변경 안정성: 코드 변경 후 회귀 여부를 더 쉽게 확인할 수 있습니다.
100% 커버리지는 계측된 항목을 모두 실행했다는 뜻일 뿐, 요구사항과 assertion이 완전하다는 뜻은 아닙니다. 모든 상황에서 달성 비용이 정당한 것도 아닙니다.
- 비용과 효율: 100% 커버리지를 달성하기 위해 비즈니스 가치가 적은 코드까지 테스트하는 것은 시간과 자원의 낭비일 수 있습니다.
- 테스트 품질: 커버리지 숫자가 높다고 해서 테스트의 품질이 높다는 것을 의미하지는 않습니다. 단순히 코드를 실행만 시킬 뿐, 올바른 시나리오나 엣지 케이스를 검증하지 않는 쓸모없는 테스트일 수도 있습니다.
- 환경 경계: 실제 인프라에서만 드러나는 실패는 통합·E2E·sandbox 검증이 필요할 수 있습니다. 정말 도달할 수 없는 코드는 제외로 숨기기보다 삭제를 먼저 검토합니다.
결론: 테스트 커버리지는 코드 품질의 지표 중 하나이지, 그 자체가 목표가 되어서는 안 됩니다.
팀 상황과 프로젝트 중요도에 맞춰 적절한 커버리지 목표를 설정하고, 양적 지표와 함께 테스트의 질적 측면도 함께 고려해야 합니다.
실패 비용과 변경 빈도가 큰 핵심 계약에는 더 강한 기준을 적용하고, 낮은 위험의 단순 코드에는 비용에 맞는 기준을 적용할 수 있습니다. 코드 종류만 보고 일률적으로 낮은 기준을 주지는 않습니다.
NestJS에서 테스트 커버리지 측정
NestJS는 Jest를 기본 테스트 러너로 사용하며, Jest는 자체 커버리지 리포트 기능을 내장하고 있습니다.
커버리지 리포트 생성NestJS 프로젝트의 package.json 파일을 보면 이미 커버리지 리포트 생성을 위한 스크립트가 정의되어 있습니다.
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
}
}기존 package.json의 scripts 객체에 위 키를 병합하고, 커버리지 측정에는 test:cov를 사용합니다.
커버리지 리포트를 생성하려면 다음 명령어를 실행합니다.
npm run test:cov명령어를 실행하면 Jest가 모든 테스트를 실행하고, 콘솔에 다음과 같은 요약 표를 출력합니다 (프로젝트에 따라 숫자는 다르게 나타날 수 있습니다):
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 92.85 | 100 | 83.33 | 92.85 |
app.controller.ts | 100 | 100 | 100 | 100 |
app.service.ts | 100 | 100 | 100 | 100 |
main.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는 파일별 퍼센트를 평균 낸 값이 아니라, 계측된 전체 항목의 covered/total을 합산해 계산한 집계입니다.
또한, 기본적으로 coverage라는 폴더가 프로젝트 루트에 생성되며, 그 안에 상세한 HTML 리포트가 포함됩니다.
coverage/lcov-report/index.html 파일을 웹 브라우저로 열면, 어떤 코드 라인이 테스트되지 않았는지 시각적으로 확인할 수 있어 테스트 개선에 큰 도움이 됩니다.
NestJS · Coverage evidence
커버리지는 테스트가 지나간 위치를 보여 주지만 동작의 정답을 증명하지는 않습니다. 전체 백분율보다 빠진 분기와 그 분기에 필요한 검증을 먼저 읽습니다.
| 지표 | 측정하는 실행 사실 | 그 자체로 증명하지 못하는 것 |
|---|---|---|
statements | 계측된 실행 문장이 한 번 이상 실행됐는가 | 결과를 올바르게 assertion했는가 |
branches | 계측된 조건의 각 분기 결과를 실행했는가 | 모든 경로 조합과 경계값을 검증했는가 |
functions | 함수나 메서드가 한 번 이상 호출됐는가 | 입력 영역과 부작용을 충분히 검사했는가 |
lines | 실행 가능한 소스 라인을 지나갔는가 | 같은 줄에 있는 모든 논리를 검증했는가 |
실행 문장을 계측한다
문장 통과는 보이지만 결과를 올바르게 검증했는지는 별도입니다.
조건 결과를 나눠 본다
각 분기 실행과 모든 경로 조합 검증은 같은 뜻이 아닙니다.
호출 여부를 센다
한 번 호출됐어도 입력 영역과 부작용 검증은 비어 있을 수 있습니다.
실행 가능한 줄을 센다
같은 줄의 복합 논리와 assertion 품질까지 보장하지는 않습니다.
누락 위치를 의미 있는 시나리오로 바꾼다
npm run test:cov로 같은 테스트 범위를 실행한다설정과 대상 파일이 같은지 먼저 고정합니다.
전체 평균에서 낮은 파일과 지표로 내려간다
HTML 리포트와
Uncovered Line #s를 함께 봅니다.빠진 줄·분기가 나타내는 업무 위험을 적는다
실패, 예외, 빈 값, 권한, 경계 조건 중 무엇이 비었는지 해석합니다.
입력과 기대 결과를 가진 테스트를 추가한다
코드를 통과시키는 호출이 아니라 관찰 가능한 결과를 검증합니다.
리포트와 테스트 실패 신호를 함께 재확인한다
숫자가 올라가도 결함을 잡지 못하면 시나리오를 다시 설계합니다.
100%도 정확성 증명은 아니다
실행된 코드가 assertion 없이 지나가거나 잘못된 요구사항을 검증할 수 있습니다.
변경 위험과 함께 읽는다
실패 비용과 변경 빈도가 큰 경계부터 누락된 테스트를 보강합니다.
커버리지의 질문은 “몇 퍼센트인가”에서 끝나지 않습니다. “어떤 위험 경로가 비었고, 어떤 assertion이 그 동작을 지킬 것인가”까지 답해야 품질 신호가 됩니다.
(위 다이어그램은 Jest 커버리지 리포트를 읽는 순서를 요약한 것입니다.)
리포트가 표시한 미실행 라인과 분기는 아직 실행되지 않은 부분입니다.
이 부분을 집중적으로 테스트 코드를 추가하여 커버리지를 높일 수 있습니다.
커버리지 임계값 설정 및 품질 관리
테스트 커버리지를 품질 관리의 일부로 사용하려면, 특정 커버리지 수준을 강제하는 임계값(Threshold)을 설정할 수 있습니다.
이는 CI/CD 파이프라인에서 커버리지가 특정 기준 미만일 경우 빌드를 실패시키는 방식으로 활용됩니다.
test:cov가 실제로 읽는 Jest 설정에 임계값 추가
프로젝트의 jest.config.js 또는 package.json의 Jest 설정에서 coverageThreshold를 추가하거나 수정합니다. 별도 E2E 설정은 test:cov 스크립트가 그 설정을 명시적으로 선택할 때만 이 실행에 적용됩니다.
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
roots: ['<rootDir>/src'], // 단위 테스트와 소스 탐색 범위
testRegex: '.*\\.spec\\.ts$', // 단위 테스트 파일만 선택
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.test.ts',
],
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,
},
},
};NestJS · Coverage quality gate
임계값은 품질의 완성선이 아니라 커버리지가 조용히 후퇴하지 않게 하는 최소선입니다. CI gate 뒤에 누락 경로와 업무 위험을 검토하는 사람이 있어야 숫자 맞추기를 피할 수 있습니다.
전체 기준과 위험 경로 기준을 분리한다
coverageThreshold: {
global: { branches: 80, lines: 80 },
'./src/users/': { lines: 80 },
'./src/app*.ts': { lines: 95 },
}
디렉터리는 하위 파일을 묶어 집계하고 glob은 각 매치 파일을 판정합니다. 특정 그룹은 나머지 global 집계에서 빠지며, 남은 파일이 없으면 global은 전체 계측 파일로 fallback합니다.
부호가 기준 단위를 바꾼다
- 양수
80: 커버된 비율이 80% 이상이어야 통과 - 음수
-10: 미커버 항목이 최대 10개여야 통과 - statement, branch, function, line을 서로 독립적으로 판정
- 경로·glob이 아무 파일도 찾지 못하면 설정 오류
- 대상 수가 작은 파일은 백분율 변화 폭이 크므로 맥락을 함께 기록
| 지표 | CI가 막는 후퇴 | 리뷰에서 이어 물을 질문 |
|---|---|---|
branches | 조건 결과가 테스트에서 사라짐 | 실패·예외·권한·빈 값 분기가 실제로 검증되는가 |
functions | 새 함수가 전혀 호출되지 않음 | 공개 계약과 부작용에 assertion이 있는가 |
lines | 핵심 줄이 실행 범위 밖에 남음 | 누락 줄이 죽은 코드인지 빠진 시나리오인지 구분했는가 |
statements | 실행 문장 범위가 낮아짐 | 통과를 위한 호출만 추가해 지표를 속이지 않았는가 |
실패 경로를 지킨다
예외·권한·빈 값 분기에 실제 결과 assertion이 있는지 검토합니다.
호출 뒤 계약을 본다
호출 횟수보다 반환·부작용·실패 계약을 함께 확인합니다.
누락 이유를 분류한다
죽은 코드인지 빠진 시나리오인지 구분한 뒤 삭제하거나 테스트합니다.
지표용 호출을 경계한다
실행만 하고 결과를 검증하지 않는 테스트는 통과 기준이 아닙니다.
최소선과 위험 검토를 같은 루프로 묶는다
팀 최소선과 핵심 경로 기준을 합의한다
CI에서 같은 설정으로 테스트와 계측을 실행한다
미달 변경을 멈추고 누락 위치를 읽는다
고위험 경계부터 테스트를 보강한다
결과와 남은 위험을 근거로 기준을 재검토한다
임계값 통과는 검토의 끝이 아닙니다. gate가 후퇴를 멈추면 리포트가 빠진 경로를 가리키고, 리뷰가 그 경로의 테스트 가치와 다음 행동을 결정합니다.
coverageThresholdglobal: 경로·glob에 매치되지 않은 파일들의 집계에 최소 커버리지 임계값을 적용합니다. 모든 파일이 특정 그룹에 매치되면 전체 계측 파일 집계로 fallback합니다.- 디렉터리 경로는 그 아래 파일을 하나의 그룹으로 집계하고, glob은 매치된 각 파일을 개별 판정하며, 파일 경로는 해당 파일만 판정합니다. 지정한 경로·glob이 아무 파일도 찾지 못하면 Jest가 오류를 냅니다.
위 숫자는 설명을 위한 예시이며 팀의 보편적 정답이 아닙니다. 양수는 최소 커버 비율이고 음수는 허용할 최대 미커버 항목 수입니다. collectCoverageFrom glob은 <rootDir>을 기준으로 해석되지만, Jest 30의 상대 coverageThreshold 경로·glob key는 Jest 프로세스의 현재 작업 디렉터리를 기준으로 해석됩니다. 위 예시는 프로젝트 루트에서 Jest를 실행해 현재 작업 디렉터리와 <rootDir>이 같은 경우입니다. global과 경로·glob 기준을 함께 두면 Jest는 지정 대상을 별도로 판정하고 그 파일의 데이터를 나머지 global 집계에서 제외합니다.
이렇게 임계값을 설정한 후 npm run test:cov를 실행했을 때, 만약 설정된 임계값을 충족하지 못하면 Jest는 에러를 발생시키고 종료됩니다.
이는 CI/CD 파이프라인에서 실패로 이어져, 테스트 커버리지가 일정 수준 이하로 떨어지는 것을 방지할 수 있습니다.
품질 관리 전략에 커버리지 활용초기 목표 설정: 프로젝트 초기에 팀과 합의하여 위험과 현재 기준선에 맞는 커버리지 최소선을 설정합니다. 수치는 프로젝트 근거로 정하고 보편적인 권장값처럼 사용하지 않습니다.
CI/CD 연동: 모든 코드 푸시 또는 풀 리퀘스트(Pull Request) 시 CI/CD 파이프라인에서 자동으로 테스트 커버리지를 측정하고 임계값을 검사하도록 설정합니다.
임계값 미달 시 빌드를 실패시켜 계측 범위가 합의한 최소선 아래로 조용히 후퇴하는 것을 막습니다. 이 gate만으로 테스트 품질 전체가 보장되지는 않습니다.
정기적인 모니터링: 커버리지 리포트를 정기적으로 확인하고, 커버리지가 낮은 부분을 식별하여 개선합니다.
코드 리뷰에 활용: 코드 리뷰 시 커버리지 리포트를 참고하여, 중요한 비즈니스 로직이나 복잡한 부분에 테스트가 충분한지 확인합니다.
테스트 문화 조성: 개발자들이 테스트 작성을 습관화하고, 커버리지를 코드 품질의 중요한 지표로 인식하도록 독려합니다.
테스트 커버리지는 실행된 코드 경로를 확인하는 지표입니다.
NestJS에서는 Jest 커버리지 명령과 임계값 설정을 사용해 측정할 수 있지만, 숫자보다 중요한 것은 실제 기능과 위험 영역을 검증하는 의미 있는 테스트입니다.
이것으로 8장 테스팅 전략을 모두 마칩니다.
단위 테스트, E2E 테스트, 커버리지 측정, CI 연동을 기준으로 테스트가 어느 경로를 검증하고 어떤 실패를 드러내는지 정리했습니다.