Vitest 단위, 컴포넌트 및 E2E 테스트 커버리지를 병합하는 방법
Source: Dev.to
Vitest 단위·컴포넌트·E2E 테스트 커버리지를 하나로 합치는 방법
프로젝트에 Vitest를 사용해 단위 테스트와 컴포넌트 테스트를 작성하고, Playwright(또는 Cypress) 같은 도구로 E2E 테스트를 수행하고 있나요?
각 테스트 러너는 자체적인 커버리지 리포트를 생성하지만, 전체 프로젝트의 커버리지를 한눈에 보기 위해서는 이 리포트들을 병합해야 합니다.
이 글에서는 다음을 다룹니다.
- Vitest와 Playwright 각각에서 커버리지 파일을 생성하는 방법
c8(또는nyc)를 이용해 JSON 커버리지 파일을 병합하는 방법- 병합된 결과를
cobertura,lcov,html등 원하는 포맷으로 출력하는 방법
1️⃣ 프로젝트 설정
1.1 package.json에 필요한 의존성 추가
npm i -D vitest @vitest/coverage-istanbul c8
# Playwright를 E2E 테스트에 사용한다면
npm i -D @playwright/test
1.2 Vitest 설정 (vite.config.ts)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import istanbul from 'vite-plugin-istanbul';
export default defineConfig({
plugins: [
vue(),
istanbul({
// 테스트 실행 시 자동으로 커버리지 파일을 생성
include: 'src/**/*.ts',
exclude: ['node_modules', 'test'],
extension: [ '.js', '.ts', '.vue' ],
requireEnv: false,
}),
],
test: {
// Vitest가 커버리지를 수집하도록 설정
coverage: {
provider: 'istanbul',
reporter: ['json', 'html'],
},
},
});
핵심:
vite-plugin-istanbul은 Vitest가 실행될 때coverage-final.json파일을./coverage폴더에 생성합니다.
1.3 Playwright 설정 (playwright.config.ts)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
reporter: [['list']],
use: {
// 브라우저 환경에서 커버리지를 수집하도록 설정
launchOptions: {
// Chrome/Chromium 기반 브라우저에만 적용
args: ['--js-flags=--experimental-modules'],
},
// Playwright가 자동으로 커버리지 파일을 저장하도록 함
coverage: {
enabled: true,
// 커버리지 파일이 저장될 디렉터리
outputFolder: 'coverage/e2e',
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
주의: Playwright 1.38+부터
coverage옵션이 기본 제공됩니다. 구버전이라면c8을 직접 래핑해야 합니다.
2️⃣ 각각의 테스트 실행 및 커버리지 파일 생성
# ① 단위·컴포넌트 테스트 (Vitest)
npm run test:unit # => ./coverage/unit/coverage-final.json
# ② E2E 테스트 (Playwright)
npm run test:e2e # => ./coverage/e2e/coverage-final.json
Tip:
npm run test:unit스크립트 예시"scripts": { "test:unit": "vitest run --coverage" }
3️⃣ 커버리지 파일 병합
3.1 c8 사용 (추천)
# 모든 JSON 커버리지 파일을 하나로 합침
npx c8 merge ./coverage/**/coverage-final.json -o ./coverage/merged/coverage.json
3.2 nyc 사용 (대안)
# nyc가 제공하는 merge 명령
npx nyc merge ./coverage/**/coverage-final.json ./coverage/merged/coverage.json
핵심:
./coverage/**/coverage-final.json은unit,component,e2e각각이 만든 파일을 모두 포함합니다.
4️⃣ 병합된 커버리지 리포트 생성
# HTML 리포트
npx c8 report --reporter=html --temp-dir=./coverage/merged
# lcov (CI에 업로드할 때 유용)
npx c8 report --reporter=lcov --temp-dir=./coverage/merged
# cobertura (SonarQube 등)
npx c8 report --reporter=cobertura --temp-dir=./coverage/merged
생성된 리포트는 ./coverage/merged 디렉터리 아래에 위치합니다. index.html을 열면 전체 프로젝트에 대한 커버리지를 한눈에 확인할 수 있습니다.
5️⃣ CI 파이프라인에 적용하기
GitHub Actions 예시:
name: Test & Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Run unit & component tests
run: npm run test:unit
- name: Run E2E tests
run: npm run test:e2e
- name: Merge coverage
run: npx c8 merge ./coverage/**/coverage-final.json -o ./coverage/merged/coverage.json
- name: Generate lcov report
run: npx c8 report --reporter=lcov --temp-dir=./coverage/merged
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/merged/lcov.info
위와 같이 설정하면 PR마다 전체 커버리지가 자동으로 계산되고, Codecov(또는 Coveralls)와 같은 서비스에 업로드됩니다.
🎉 마무리
- Vitest와 Playwright(또는 다른 E2E 프레임워크) 각각에서 커버리지를 수집한다.
c8(또는nyc)을 이용해 JSON 파일을 병합한다.- 병합된 데이터를 기반으로 HTML, lcov, cobertura 등 원하는 포맷의 리포트를 만든다.
이 과정을 자동화하면 팀 전체가 동일한 커버리지 기준을 공유할 수 있어, 테스트 품질을 지속적으로 향상시킬 수 있습니다.
궁금한 점이 있으면 언제든 댓글로 알려 주세요! 🚀
현대 React 애플리케이션은 종종 다양한 환경에서 여러 종류의 테스트를 실행합니다. 각 테스트는 자체 커버리지 보고서를 생성합니다. 이 기사에서는 이를 단일하고 정확한 커버리지 보고서로 병합하는 방법을 보여줍니다.
기술 스택
| 계층 | 기술 |
|---|---|
| 애플리케이션 | React + Next.js / Vite / Webpack |
| 단위 테스트 | Vitest (jsdom environment) |
| 컴포넌트 테스트 | Vitest Browser Mode + Playwright |
| E2E 테스트 | Playwright + nextcov |
| 커버리지 제공자 | V8 |
문제: 별도 커버리지 보고서
일반적인 Next.js 프로젝트는 다음과 같은 구조를 가질 수 있습니다:
coverage/
├── unit/ # From vitest unit tests (jsdom)
├── component/ # From vitest browser tests
└── e2e/ # From playwright + nextcov
각 디렉터리에는 동일한 소스 파일에 대한 커버리지 데이터가 포함된 coverage-final.json 파일이 있습니다. 이를 단순히 병합하면 잘못된 수치가 나옵니다.
왜 순진한 병합이 실패하는가
// src/components/Input.tsx
'use client'
import React from 'react'
export function Input({ error }: { error?: string }) {
return (
<div>
{error && <span>{error}</span>}
</div>
)
}
- Vitest 단위 테스트는
'use client'지시문과import문을 실행 가능한 문으로 계산합니다. - E2E 테스트 중 V8 커버리지는 해당 라인이 제거된 번들된 코드에서 실행됩니다.
이 차이를 고려하지 않고 병합하면 문장 수가 부풀어 오르고 커버리지 비율이 왜곡됩니다.
Source: …
별도의 커버리지 디렉터리 설정
단위 테스트 구성
// vitest.config.mts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
include: ['src/**/__tests__/**/*.test.{ts,tsx}'],
exclude: ['src/**/*.browser.test.{ts,tsx}'],
coverage: {
enabled: true,
provider: 'v8',
reportsDirectory: './coverage/unit',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/__tests__/**',
'src/**/*.test.{ts,tsx}',
'src/**/*.browser.test.{ts,tsx}',
],
reporter: ['text', 'json', 'html'],
},
},
})
핵심 설정
reportsDirectory: './coverage/unit'– 전용 디렉터리로 출력provider: 'v8'– V8 커버리지 사용 (Istanbul이 아님)reporter: ['json']–coverage-final.json을 생성하려면json포함 필요
컴포넌트 테스트 구성
// vitest.component.config.mts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'
import tsconfigPaths from 'vite-tsconfig-paths'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
globals: true,
setupFiles: ['./vitest.browser.setup.ts'],
include: ['src/**/*.browser.test.{ts,tsx}'],
coverage: {
enabled: true,
provider: 'v8',
reportsDirectory: './coverage/component',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/__tests__/**',
'src/**/*.test.{ts,tsx}',
'src/**/*.browser.test.{ts,tsx}',
],
reporter: ['text', 'json', 'html'],
},
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
headless: true,
},
},
})
단위 테스트와의 주요 차이점
include: ['src/**/*.browser.test.{ts,tsx}']– 파일 패턴이 다름reportsDirectory: './coverage/component'– 별도의 출력 디렉터리browser.enabled: true– 실제 브라우저에서 실행
package.json 스크립트
{
"scripts": {
"test:unit": "vitest run",
"test:component": "vitest run --config vitest.component.config.mts",
"test": "npm run test:unit && npm run test:component",
"coverage:merge": "nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged"
}
}
Directive 스트리핑 문제
V8 커버리지 카운트가 환경마다 다릅니다:
| 환경 | 실행 가능한 것으로 간주되는 항목 |
|---|---|
| Vitest 유닛 테스트 (jsdom) | import 문과 디렉티브 ('use client', 'use server') |
| Vitest 브라우저 테스트 | 유닛 테스트와 동일 |
| E2E 커버리지 (번들링) | 번들링 과정에서 디렉티브와 import가 제거됨 |
정규화 없이
# Vitest 유닛 커버리지 (디렉티브 포함)
Input.tsx: 10 statements, 8 covered (80%)
# E2E 커버리지 (번들에 디렉티브 없음)
Input.tsx: 8 statements, 6 covered (75%)
# 순진한 병합 (잘못된 결과!)
Input.tsx: 10 statements, 14 covered (140%?!)
해결책은 병합하기 전에 import 문과 디렉티브를 제거하여 모든 소스를 동일한 기준선으로 정규화하는 것입니다.
왜 Vitest의 멀티‑프로젝트 설정을 사용하지 않을까?
Vitest의 멀티‑프로젝트 설정은 단위 테스트와 컴포넌트 테스트를 별개의 프로젝트로 실행하고 커버리지를 자동으로 병합할 수 있습니다. 안타깝게도, Vitest의 버그 때문에 올바른 병합이 이루어지지 않습니다. 이 문제가 해결될 때까지는 외부 도구가 필요합니다.
옵션 1: vitest-coverage-merge (Vitest 전용)
설치
npm install -D vitest-coverage-merge
병합 명령
npx vitest-coverage-merge coverage/unit coverage/component -o coverage/merged
동작 방식
- 각 디렉터리에서
coverage-final.json을 로드합니다. - ESM import 문과 React/Next.js 지시자를 제거하여 정규화합니다.
- 브라우저‑테스트 구조를 우선시하면서 지능적으로 병합합니다.
- HTML, LCOV, JSON 보고서를 생성합니다.
이 도구는 jsdom과 실제 브라우저 테스트 간의 환경 기반 커버리지 차이를 해결하도록 설계되었습니다—Vitest 내장 --merge-reports가 샤드된 테스트 실행만 처리하는 것과 달리.
업데이트된 스크립트 (Vitest 전용)
{
"scripts": {
"test:unit": "vitest run",
"test:component": "vitest run --config vitest.component.config.mts",
"test": "npm run test:unit && npm run test:component",
"coverage:merge": "vitest-coverage-merge coverage/unit coverage/component -o coverage/merged"
}
}
옵션 2: nextcov merge (E2E 포함)
Vitest 커버리지를 Playwright + nextcov E2E 커버리지와 병합하려면 nextcov를 사용하세요:
npx nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged
동작 방식
- 각 디렉터리에서
coverage-final.json을 로드합니다 - 기본적으로 지시문을 제거합니다 (
'use client','use server', import 문) - 각 파일에 대해 최적의 구조를 선택합니다 (지시문이 과도하게 삽입되지 않은 소스를 선호)
- “max” 전략을 사용해 실행 횟수를 병합합니다
- HTML, LCOV, JSON, 텍스트‑요약 보고서를 생성합니다
지시문 제거 비활성화
소스에 지시문 불일치가 없을 경우:
npx nextcov merge coverage/unit coverage/component --no-strip
E2E 커버리지 추가
Playwright 설정 (playwright.config.ts)
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import type { NextcovConfig } from 'nextcov';
export const nextcov: NextcovConfig = {
cdpPort: 9230,
outputDir: 'coverage/e2e',
sourceRoot: './src',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/__tests__/**',
'src/**/*.test.{ts,tsx}',
'src/**/*.browser.test.{ts,tsx}',
],
reporters: ['html', 'lcov', 'json', 'text-summary'],
};
export default defineConfig({
testDir: './e2e',
reporter: [['list'], ['html'], ['./e2e/coverage-reporter.ts']],
use: {
baseURL: 'http://localhost:3000',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
E2E 스크립트
{
"scripts": {
"build:e2e": "cross-env E2E_MODE=true npm run build",
"start:e2e": "cross-env E2E_MODE=true NODE_V8_COVERAGE=.v8-coverage NODE_OPTIONS=--inspect=9230 next start",
"e2e": "npm run e2e:clean && npm run e2e:run",
"e2e:clean": "rimraf coverage/e2e",
"e2e:run": "start-server-and-test start:e2e http://localhost:3000 playwright-test",
"coverage:merge": "nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged"
}
}
자세한 E2E 커버리지 설정은 Article 1을 참고하세요.
병합 전략 세부 정보
최대 전략 (기본)
각 커버리지 항목(문, 분기, 함수)에 대해 nextcov는 모든 소스에서 최대 카운트를 사용합니다:
Unit tests: line 10 executed 5 times
Component tests: line 10 executed 3 times
E2E tests: line 10 executed 2 times
-------------------------------------------------
Merged: line 10 executed 5 times (max)
이 보수적인 접근 방식은 중복 계산 없이 관찰된 가장 높은 커버리지를 반영합니다.
구조 선택
병합 시, nextcov는 어떤 소스의 구조를 사용할지 선택합니다(어떤 문이 존재하고, 분기가 어디에 있는지). 선호 순서:
- L1:0 지시문이 없는 소스(E2E 스타일 커버리지)
- 더 많은 커버리지 항목을 가진 소스(보다 완전한 분석)
- 동등할 경우 나중에 나온 소스(E2E가 보통 마지막)
이는 지시문 추적 소스로 인한 과도한 문 카운트를 방지합니다.
전체 예시
디렉터리 구조
project/
├── src/
│ ├── components/
│ │ ├── Input.tsx
│ │ └── __tests__/
│ │ ├── Input.test.tsx # Unit tests
│ │ └── Input.browser.test.tsx # Component tests
├── e2e/
│ └── form.spec.ts # E2E tests
├── coverage/
│ ├── unit/
│ ├── component/
│ ├── e2e/
│ └── merged/
├── vitest.config.mts
├── vitest.component.config.mts
├── playwright.config.ts
└── package.json
테스트 실행 및 병합
# Run all tests with coverage
npm run test:unit
npm run test:component
npm run e2e
# Merge all coverage
npx nextcov merge coverage/unit coverage/component coverage/e2e -o coverage/merged
# Or use the npm script
npm run coverage:merge
샘플 출력
📊 nextcov merge
Inputs: coverage/unit, coverage/component, coverage/e2e
Output: coverage/merged
Reporters: html, lcov, json, text-summary
Strip directives: yes
Loading: coverage/unit/coverage-final.json
Loading: coverage/component/coverage-final.json
Loading: coverage/e2e/coverage-final.json
Merged: 1234 statements, 567 branches, 89 functions
Reports written to coverage/merged
커버리지 요약
: coverage/component/coverage-final.json
Loading: coverage/e2e/coverage-final.json
Stripped: 30 imports, 28 directives
=============================== Coverage summary ===============================
Statements : 89.07% ( 595/668 )
Branches : 78.06% ( 338/433 )
Functions : 92.90% ( 131/141 )
Lines : 88.71% ( 574/647 )
===============================================================================
✅ 병합된 커버리지 보고서가 생성되었습니다
Output: coverage/merged
보고서 형식 선택
기본적으로 nextcov는 모든 형식을 생성합니다. 필요에 따라 맞춤 설정하세요:
# Only HTML and LCOV (for CI integration)
npx nextcov merge coverage/unit coverage/e2e --reporters html,lcov
# Only JSON (for further processing)
npx nextcov merge coverage/unit coverage/e2e --reporters json
사용 가능한 리포터
| Reporter | Description |
|---|---|
html | 인터랙티브 HTML 보고서 |
lcov | CI 도구용 LCOV 형식 (Codecov 등) |
json | JSON 형식 (coverage-final.json) |
text-summary | 콘솔 요약 |
문제 해결
“No coverage‑final.json 찾을 수 없음”
Vitest 설정에 json이 리포터에 포함되어 있는지 확인하세요:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
reporter: ['text', 'json', 'html'], // Must include 'json'
},
},
});
병합 후 커버리지 비율이 이상하게 보임
일반적으로 지시문 스트리핑이 예상대로 작동하지 않아 발생합니다. 확인하세요:
- 모든 소스가 V8 커버리지를 사용하고 있는지 (
provider: 'v8') - 소스 파일에
'use client'또는'use server'지시문이 포함되어 있는지 --no-strip옵션으로 병합하여 원시 데이터를 확인해 보세요
병합된 보고서에 일부 파일이 누락됨
파일은 최소 하나의 소스에 존재할 때만 표시됩니다. E2E 테스트가 파일을 전혀 다루지 않더라도 유닛/컴포넌트 커버리지에서 포함됩니다.
요약
| 단계 | 명령 | 출력 |
|---|---|---|
| 단위 테스트 | npm run test:unit | coverage/unit/ |
| 컴포넌트 테스트 | npm run test:component | coverage/component/ |
| E2E 테스트 | npm run e2e | coverage/e2e/ |
| 모두 병합 | npx nextcov merge coverage/unit coverage/component coverage/e2e | coverage/merged/ |