8편. Vitest + V8 기반 테스트 커버리지 : @vitest/coverage-v8
📚 목차
1. 테스트 커버리지는 왜 중요한가?
2. Vitest에서 커버리지 활성화하기: @vitest/coverage-v8 설정
3. 커버리지 리포트 실습 및 예제
4. 커버리지 리포트 확인
📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /vitest
1. 테스트 커버리지는 왜 중요한가?
1. 테스트 커버리지란?
테스트 커버리지(Test Coverage)는 전체 애플리케이션 코드 중 테스트로 실행된 코드의 비율을 의미합니다.
즉, 작성된 테스트 코드가 실제 애플리케이션의 어느 부분까지 영향을 미치고 있는지를 정량적으로 측정하는 지표입니다.
2. 주요 커버리지 지표 및 필요한 이유
| 지표 | 설명 |
| Statements | 전체 코드 중 실행된 명령문(statement)의 비율 |
| Branches | if, else, switch, ? : 등의 조건 분기문 중 테스트된 경로 비율 |
| Functions | 정의된 함수 또는 메서드가 호출된 비율 |
| Lines | 전체 코드 라인 중 테스트를 거친 물리적 코드 줄 수의 비율 |
▸ QA 및 유지보수: 테스트되지 않은 '사각지대'를 찾아내어 잠재적인 버그를 예방합니다.
▸ 리팩토링 자신감: 높은 커버리지는 코드 구조를 변경할 때 기존 로직이 망가지지 않았음을 보장하는 안전장치가 됩니다.
▸ 협업 기준: 팀 내 최소 커버리지 기준(Threshold)을 정해 코드 품질을 상향 평준화할 수 있습니다.
📌 실무 포인트: 커버리지는 100%가 목표가 아닙니다
커버리지는 100%일 필요는 없습니다.
하지만 핵심 로직은 높은 커버리지를 유지해야 안정적인 서비스 운영이 가능합니다.
테스트 커버리지를 무조건 100%로 맞추는 것은 비효율적일 수 있습니다.
테스트하기 어려운 코드 (예: 로깅, 예외 메시지 등)까지 억지로 테스트하려 하면 오히려 코드 품질이 낮아지거나 테스트 유지 비용이 높아집니다.
2. Vitest에서 커버리지 활성화하기: @vitest/coverage-v8 설정
📌V8 기반 커버리지란?
Vitest는 내부적으로 V8 JavaScript 엔진의 native coverage API 를 사용하여 빠르고 정확한 커버리지를 수집할 수 있습니다.
이를 위해 별도 패키지인 @vitest/coverage-v8이 필요합니다.
1. @vitest/coverage-v8 패키지 설치
cd nodejs-tutorials\vitest
npm install -D @vitest/coverage-v8
== 설치 결과 ==
PS D:\NodejsDevelope\workspace\nodejs-tutorials\vitest> npm list
nodejs-tutorials@1.0.0 D:\NodejsDevelope\workspace\nodejs-tutorials
└─┬ vitest-study@1.0.0 -> .\vitest
├── @vitest/coverage-v8@4.0.17
├── @vitest/ui@4.0.16
├── axios@1.13.2
├── fastify@5.6.2
├── msw@2.12.7
├── undici@7.18.2
└── vitest@4.0.16
2. vitest.config.ts 설정
프로젝트 루트의 설정 파일에서 커버리지 옵션을 활성화합니다.
coverage: {
enabled: true, // 테스트 실행 시 커버리지 수집을 활성화합니다.
provider: 'v8', // V8 엔진의 native coverage API를 사용하는 공급자 (빠르고 정확함)
reporter: ['text', 'json', 'html'], // 생성할 커버리지 리포트 형식
reportsDirectory: './coverage', // 커버리지 리포트가 저장될 디렉터리 경로
// 커버리지 측정 대상 파일을 지정합니다.
// 보통 테스트 대상 소스코드를 포함하는 경로를 지정합니다.
include: ['src/**/*.ts'],
// 커버리지에서 제외할 파일 목록입니다.
// 진입점(main.ts), 타입 정의(d.ts), 테스트 파일 등은 일반적으로 제외합니다.
exclude: [
'src/main.ts', // 앱의 엔트리포인트 (Fastify/Express 부트스트랩 코드 등)
'**/*.d.ts', // 타입 선언 파일은 실행 코드가 아니므로 제외
'src/types/**', // 공용 타입 모음 디렉토리
'**/*.test.ts', // 테스트 자체는 커버리지 측정 대상이 아님
],
// 테스트 실패 조건으로 사용할 최소 커버리지 기준을 설정합니다.
// 기준 미달 시 CI/CD 파이프라인에서 실패로 처리할 수 있습니다.
thresholds: {
lines: 80, // 전체 코드 줄 수 기준 80% 이상 실행되어야 통과
functions: 80, // 함수 기준 80% 이상 호출
branches: 70, // 조건 분기 기준 70% 이상 테스트 커버
statements: 80, // 실행 가능한 명령문 기준 80% 이상 커버
},
},
3. 커버리지 리포트 실습 및 예제
1. 커버리지 실행 명령어
npx vitest --coverage
▸--coverage: 커버리지 수집 기능을 활성화합니다. vitest.config.ts의 coverage 설정을 기반으로 동작합니다.
| 옵션 | 설명 |
| --coverage | 커버리지를 수집합니다. (필수) |
| --run | watch 모드를 비활성화하고 테스트를 한 번만 실행합니다. 커버리지 사용 시 필요 |
| --dir [경로] | 테스트 실행의 루트 디렉터리를 지정합니다. (기본: 현재 디렉터리) |
| --reporter [형식] | 테스트 결과 출력 형식을 지정합니다 (default, verbose, dot, json 등) |
| --config [파일명] | 커스텀 vitest 설정 파일을 사용할 때 사용 (vitest.config.custom.ts 등) |
| --testNamePattern [정규식] | 특정 테스트 이름에 해당하는 케이스만 실행합니다 |
| --passWithNoTests | 실행할 테스트가 없더라도 오류 없이 통과시킵니다 (CI에서 유용) |
예시)
1. 특정 테스트 파일만 실행하면서 커버리지 수집
npx vitest run src/services/user.service.test.ts --coverage
2. 특정 테스트 이름만 실행
npx vitest run --coverage --testNamePattern="should create user"
3. 커스텀 설정 파일 사용
npx vitest run --coverage --config=vitest.coverage.config.ts
2. 실습용 예시 코드
// src/ch08/user.service.ts
export interface User {
id: number;
name: string;
age: number;
}
// 사용자의 나이를 검증하는 함수입니다.
export const validateUserAge = (user: User): boolean => {
if (user.age < 0) {
// 음수 나이는 유효하지 않으므로 예외 처리합니다.
throw new Error("Age cannot be negative");
}
// 만 19세 이상이면 true, 그렇지 않으면 false를 반환합니다.
return user.age >= 19;
}
✔️ Statements (총 4개)
▸ 함수 선언 → const validateUserAge = (user) => {...}
▸ 조건문 평가 → if (user.age < 0)
▸ 예외 처리 → throw new Error(...)
▸ 반환 → return user.age >= 19
✔️ Branches (총 2개)
▸ 조건문: if (user.age < 0)
→ 이 조건은 true일 수도 있고 false일 수도 있음
→ 각각 하나의 분기로 계산됨
✔️ Functions (총 1개)
▸ validateUserAge() 함수 하나만 존재
✔️ Lines (총 4줄)
▸ export const validateUserAge = ... → 함수 정의
▸ if (user.age < 0)
▸ throw new Error(...)
▸ return user.age >= 19
// tests/ch08/user.service.test.ts
import { describe, it, expect } from 'vitest';
import { validateUserAge } from '../../src/ch08/user.service';
describe('validateUserAge', () => {
it('성인인 경우 true를 반환해야 한다', () => {
const user = { id: 1, name: 'Alice', age: 20 };
expect(validateUserAge(user)).toBe(true);
});
// 누락된 케이스: 미성년자(false 반환), 음수 나이(에러 발생)
});
3. 테스트 범위(커버리지 대상) 제한하기
실습 단위로 커버리지를 확인할 때는 전체 프로젝트를 대상으로 하기보다 특정 폴더만 포함하는 것이 좋습니다.
// vitest.config.ts
coverage: {
provider: 'v8',
include: ['src/ch08/**/*.ts'], // ✅ ch08 하위 소스만 커버리지 대상으로 설정
exclude: ['**/*.test.ts', '**/*.d.ts'],
reporter: ['text', 'html'],
}
4. 커버리지 결과 해석
npx vitest .\tests\ch08/user.service.test.ts --coverage

테스트를 실행하면 Branch 커버리지가 100%가 되지 않습니다.
if (user.age < 0) 조건의 참인 경우와 성인이 아닌 경우를 테스트하지 않았기 때문입니다.
if (user.age < 0) { ... }
return user.age >= 19;
> user.age < 0 인 경우 (예외 발생)
> user.age >= 19 인 경우 (true)
> user.age < 19 인 경우 (false)
총 3개의 논리 분기 중 현재 테스트는 true 분기만 실행하고 있기 때문입니다.
즉, 커버리지는 단순히 “라인이 실행됐는지”가 아니라 모든 조건 분기가 실행됐는지를 기준으로 측정됩니다.
5. 누락된 테스트 보완 → Branch 100% 달성
it('미성년자인 경우 false를 반환해야 한다', () => {
const user = { id: 2, name: 'Bob', age: 15 };
expect(validateUserAge(user)).toBe(false);
});
it('나이가 음수면 에러를 던져야 한다', () => {
const user = { id: 3, name: 'Charlie', age: -1 };
expect(() => validateUserAge(user)).toThrow("Age cannot be negative");
});
이제 모든 분기(Branch)가 실행되어 100% 커버리지를 달성할 수 있습니다.

4. 커버리지 리포트 확인
Vitest에서는 커버리지 리포트를 여러 형식으로 출력할 수 있으며, 결과 파일이 어디에 저장될지도 직접 지정할 수 있습니다.
이 기능은 특히 로컬 디버깅, CI 리포트 확인, 시각적 분석 등 다양한 상황에서 유용하게 활용됩니다.
// vitest.config.ts
coverage: {
reporter: ['text', 'json', 'html'], // 리포트 출력 형식 지정
reportsDirectory: './coverage', // 리포트 저장 경로 지정
}
| 옵션 | 설명 |
| 'text' | 터미널에 요약 형태의 커버리지 결과를 출력합니다. 빠르게 수치를 확인할 때 유용합니다. |
| 'json' | coverage-final.json 형태로 리포트를 생성하며, Codecov나 Coveralls 같은 외부 리포팅 툴에 활용할 수 있습니다. |
| 'html' | 시각적으로 분석 가능한 HTML 리포트를 생성합니다. 브라우저에서 열어 누락된 테스트 라인을 한눈에 확인할 수 있습니다. |
💡html 리포트 화면 예시


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Vitest&TypeBox' 카테고리의 다른 글
| [TypeBox] 2편. Fastify를 위한 TypeBox(legacy) 기본 문법 이해하기 (0) | 2026.02.23 |
|---|---|
| [TypeBox] 1편. Fastify에서 TypeBox를 사용하는 이유와 적용 방법 (0) | 2026.02.19 |
| [Vitest] 7편. Vitest Mocking 이해하기 : vi.mock()과 vi.fn() (0) | 2026.02.10 |
| [Vitest] 6편. MSW(Mock Service Worker) + Vitest 실무 API 테스트 (0) | 2026.02.06 |
| [Vitest] 5편. Vitest로 외부 API 테스트하기: axios, undici (0) | 2026.02.05 |