[DevTools] ๋ฆฌ์กํธ ํ ์คํธ ํ๊ฒฝ(Vitest, React Testing Library) ๋ฐ CI ๊ตฌ์ถ
์ฝ๋ ํ์ง์ ๋ณด์ฅํ๊ณ , ๊ธฐ๋ฅ์ด ์๋ํ ๋๋ก ๋์ํ๋์ง ํ์ธํ๊ธฐ ์ํด ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค. ํนํ ๊ธฐ๋ฅ ์ถ๊ฐ๋ ๋ฆฌํฉํ ๋ง์ ํ ๋ ํ ์คํธ ์ฝ๋๊ฐ ์์ผ๋ฉด ๊ธฐ์กด ๊ธฐ๋ฅ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ์ฝ๊ฒ ํ์ธํ ์ ์๊ณ , ์ฌ๋ฆฌ์ ์ธ ์์ ๊ฐ์ ์ฃผ๋ ์ฅ์ ๋ ์๋ค. ํ๋ก ํธ์๋ ํ ์คํธ ์ข ๋ฅ๋ ํฌ๊ฒ ๋จ์ ํ ์คํธ, ํตํฉ ํ ์คํธ, E2E ํ ์คํธ, ์ ์ ํ ์คํธ๋ก ๋๋๋ค.
ํ ์คํธ ์ข ๋ฅ | ์ค๋ช | ์์ | ์ฃผ์ ๋๊ตฌ |
๋จ์ ํ ์คํธ(Unit Test) | ๊ฐ๋ณ ํจ์, ์ปดํฌ๋ํธ, ๋ชจ๋์ ๋์ ๊ฒ์ฆ | ๋ฒํผ ํด๋ฆญ ์ ํน์ ํจ์ ํธ์ถ ์ฌ๋ถ | Jest, Vitest, Mocha, Jasmine ๋ฑ |
ํตํฉ ํ ์คํธ(Integration Test) | ์ฌ๋ฌ ๋ชจ๋์ด ํจ๊ป ์ ์๋ํ๋์ง ํ์ธ | ์ํ ๊ตฌ๋งค ์ ์์ก ์ ๋ฐ์ดํธ, ์ฌ๊ณ ๋ณ๊ฒฝ | Jest, Vitest, React Testing Library ๋ฑ |
E2E ํ ์คํธ(End To End Test) | ์ฌ์ฉ์ ๊ด์ ์์ ์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์ ์ ํ๋ฆ ํ ์คํธ | ๋ก๊ทธ์ธ๋ถํฐ ์ํ ๊ตฌ๋งค๊น์ง์ ์ ์ฒด ๊ณผ์ | Cypress, Selenium, Playwright, Puppeteer ๋ฑ |
์ ์ ํ ์คํธ(Static Test) | ์ฝ๋ ์คํ ์์ด ์์ค ์ฝ๋ ์์ฒด ๋ถ์ | TypeScript ํ์ ๊ฒ์ฌ, ESLint ์ฝ๋ ์คํ์ผ ๊ฒ์ฆ | ESLint, Prettier, TypeScript ๋ฑ |
์๋์์ Vitest, React Testing Library๋ฅผ ์ด์ฉํด ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด์(Vite, React, TypeScript ๊ธฐ๋ฐ).
Vitest๋ Vite ๊ธฐ๋ฐ ํ ์คํธ ํ๋ ์์ํฌ๋ก Vite ์ค์ ๊ณผ ํ๋ฌ๊ทธ์ธ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๊ณ , ๋๋ถ๋ถ์ Jest API์ ํธํ๋๋ค. React Testing Library๋ ๋ฒํผ ํด๋ฆญ, ํ ์คํธ ์ ๋ ฅ ๋ฑ ์ปดํฌ๋ํธ๋ฅผ ์ค์ ์ฌ์ฉ์์ฒ๋ผ ์ํธ์์ฉํ๋ฉฐ ํ ์คํธํ๋๋ก ์ค๊ณ๋ ๋๊ตฌ๋ค.
ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถ
๐ก ํจํค์ง ๋งค๋์ ๋ pnpm์ ์ฌ์ฉํ๋ค.
๊ธฐ๋ณธ ์ค์
โถ ํจํค์ง ์ค์น
pnpm i -D vitest @testing-library/react @testing-library/jest-dom jsdom
- vitest : vite ๊ธฐ๋ฐ ํ ์คํธ ๋ฌ๋
- @testing-library/react : ์ปดํฌ๋ํธ ํ ์คํธ ํจํค์ง
- @testing-library/jest-dom : DOM ์์์ ํน์ ์ํ/์์ฑ ํ ์คํธ ํจํค์ง(๋งค์ฒ)
- jsdom : Node์์ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์ ํจํค์ง
โท vite.config.ts ํ์ผ ์์
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// describe, it ๋ฑ ์ ์ญ ํ
์คํธ ํจ์๋ฅผ import ์์ด ์ฌ์ฉํ ์ ์๋๋ก ์ค์
globals: true,
// jsdom ํ๊ฒฝ์ ์ฌ์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์
environment: 'jsdom',
// ํ
์คํธ ์คํ ์ ํน์ ํ์ผ์ด๋ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋๋ก ์ง์
setupFiles: ['./src/setup-tests.ts'],
},
});
โธ src ํด๋์ setup-tests.ts ํ์ผ ์์ฑ
import * as matchers from '@testing-library/jest-dom/matchers';
import { expect } from 'vitest';
import { cleanup } from '@testing-library/react';
/**
* jest-dom ์์ ์ ๊ณตํ๋ ๋งค์ฒ๋ฅผ Vitest expect ํจ์์์ ์ฌ์ฉํ ์ ์๋๋ก ํ์ฅ
* ๋งค์ฒ๋ toBeInTheDocument ๋ฑ๊ณผ ๊ฐ์ DOM ์ํ๋ฅผ ๊ฒ์ฆํ๋ ๋ฉ์๋
* */
expect.extend(matchers);
afterEach(() => {
cleanup(); // ๊ฐ ํ
์คํธ ์ข
๋ฃ ํ DOM ์ ๋ ๋๋ง๋ ์์ ์ ๊ฑฐ
});
โน package.json ์คํฌ๋ฆฝํธ ์ถ๊ฐ (์ค์ ์pnpm test
๋ช
๋ น์ด๋ก ํ
์คํธ ์คํ ๊ฐ๋ฅ)
{
"scripts": {
// ...
"test": "vitest"
}
}
โบ tsconfig.app.json ์์ (Vitest, Testing Library ํ์ ์ ์ ์ถ๊ฐ)
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vitest/globals", "@testing-library/jest-dom"] // ์ถ๊ฐ
// ...
},
"include": ["src"]
}
์์ฒ๋ผ ํ์ ์ ์๋ฅผ ์ถ๊ฐํด ๋๋ฉด TS2582 ์๋ฌ๊ฐ ์ฌ๋ผ์ง๋ค.
ํ ์คํธ ์ฝ๋ ์์ฑ
๐ก getByRole ํจ์์ name
์ต์
์ ์ ๊ทผ์ฑ ์ด๋ฆ์ ๊ฐ๋ฆฌํค๋ฉฐ, ์๋ ๋ฐฉ๋ฒ์ผ๋ก ์ค์ ๋ ์ ์๋ค.
App.test.tsx ํ์ผ ์์ฑ → ์๋ ํ
์คํธ ์ฝ๋๋ฅผ ์ถ๊ฐํ ํ ํฐ๋ฏธ๋์ pnpm test
๋ฅผ ์
๋ ฅํด ๋ณด์. 1 passed ํ์๊ฐ ๋์ค๋ฉด ํ
์คํธ ํ๊ฒฝ์ค์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋ ๊ฒ์ด๋ค.
// App.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import App from './App.tsx';
describe('App component', () => {
test('increments count when button is clicked', () => {
render(<App />);
const button = screen.getByRole('button', { name: /count is \d/i });
fireEvent.click(button);
expect(button).toHaveTextContent('count is 1');
});
});
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ ํ ์คํธ ์ผ์ด์ค๊ฐ ์์ค ์ฝ๋์ ์ผ๋ง๋ ๋ง์ ๋ถ๋ถ์ ์คํํ๋์ง ๋ฐฑ๋ถ์จ๋ก ๋ํ๋ธ ์งํ๋ค. ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ ์ฃผ๋ก ์๋ ์ธก์ ๊ธฐ์ค์ ํตํด ํ๊ฐ๋๋ค.
- Statement Coverage : ๊ฐ ๊ตฌ๋ฌธ์ด ์ต์ 1ํ ์ด์ ์คํ๋๋์ง ์ธก์
- Branch Coverage : ์กฐ๊ฑด๋ฌธ(if/else)์ ๊ฐ ๋ถ๊ธฐ๊ฐ ์คํ๋๋์ง ์ธก์
- Function Coverage : ์ ์๋ ํจ์๋ค์ด ์ต์ 1ํ ์ด์ ํธ์ถ๋๋์ง ์ธก์
- Line Coverage : ๊ฐ ๋ผ์ธ์ด ์ต์ 1ํ ์ด์ ์คํ๋๋์ง ์ธก์
Vitest์์๋ v8(๊ธฐ๋ณธ๊ฐ) ํน์ istanbul ๊ณต๊ธ์๋ฅผ ํตํด ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ํ์ธํ ์ ์๋ค.
โถ v8 ๊ณต๊ธ์ ์ค์น
pnpm i -D @vitest/coverage-v8
โท package.json ์คํฌ๋ฆฝํธ ์ถ๊ฐ
{
"scripts": {
// ...
"test": "vitest",
"coverage": "vitest run --coverage"
}
}
์ด์ ํฐ๋ฏธ๋์ pnpm coverage
๋ฅผ ์
๋ ฅํ๋ฉด ํ
์คํธ ํ์์ ์ปค๋ฒ๋ฆฌ์ง ํํฉ์ ์ฝ์์ ์ถ๋ ฅํ๊ณ , coverage/
๋๋ ํฐ๋ฆฌ์ index.html ๊ฐ์ ๋ณด๊ณ ์๊ฐ ์๋์ผ๋ก ์์ฑ๋๋ค.
์ปค๋ฒ๋ฆฌ์ง ๊ด๋ จ ๊ตฌ์ฑ์ vite.config.ts ํ์ผ coverage
์์ฑ์์ ๋ณ๊ฒฝํ ์ ์๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ํ
์คํธ๋ฅผ ์คํจํ์ ๋ ์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์๋ฅผ ์์ฑํ์ง ์๋๋ฐ, coverage.reportOnFailure ๊ฐ์ true
๋ก ๋ฐ๊พธ๋ฉด ํ
์คํธ ์คํจ ์์๋ ์์ฑ๋๋ค. ์ด ์ธ์๋ ์ถ๋ ฅ ๋ฐฉ์ ์ง์ ์ ์ง์ ํ๋ coverage.reporter ๋ฑ ๋ค์ํ ์ต์
์ ์ ๊ณตํ๋ค.
// vite.config.ts
// ...
export default defineConfig({
// ...
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setup-tests.ts'],
coverage: {
/** ์ปค๋ฒ๋ฆฌ์ง ๊ณต๊ธ์ ์ง์ . ๊ธฐ๋ณธ๊ฐ v8 */
provider: 'v8',
/** ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ ์ถ๋ ฅ ๋ฐฉ์ ์ง์ ('text' ์ถ๊ฐํ๋ฉด ํ
์คํธ ํ์์ผ๋ก ์ฝ์์ ์ถ๋ ฅ) */
reporter: ['text', 'html'],
/** ํ
์คํธ ์คํจ ์ ์ปค๋ฒ๋ฆฌ์ง ์์ฑ ์ฌ๋ถ */
reportOnFailure: true,
},
},
});
Vitest UI
Vitest UI๋ ํ ์คํธ ํํฉ์ ๋ณด๋ค ์ฝ๊ฒ ํ์ธํ๊ณ ์ํธ์์ฉ ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๋๊ตฌ๋ค. ์น UI๋ฅผ ํตํด ํ ์คํธ๋ฅผ ์คํํ๊ฑฐ๋ ๊ฒฐ๊ณผ๋ฅผ ์ค์๊ฐ์ผ๋ก ํ์ธํ ์ ์๊ณ , ํ ์คํธ ์ผ์ด์ค๋ฅผ ํด๋ฆญํด์ ์์ธํ ์ ๋ณด๋ฅผ ๋ณผ ์๋ ์๋ค.
โถ ํจํค์ง ์ค์น
pnpm i -D @vitest/ui
โท Vitest UI ์คํ (package.json ํ์ผ์ test:ui
๋ฑ ๋ช
๋ น์ด๋ก ์
๋ ฅํด ๋๋ฉด ํธํ๋ค)
vitest --ui
โธ Vitest UI์์ ์ปค๋ฒ๋ฆฌ์ง ํํฉ๋ ๋ณด๊ณ ์ถ๋ค๋ฉด vite.config.ts ํ์ผ์ enabled
์์ฑ์ true
๋ก ๋ณ๊ฒฝํ๋ค.
// vite.config.ts
// ...
export default defineConfig({
// ...
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setup-tests.ts'],
coverage: {
/** ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ๋ฅ ํ์ฑ */
enabled: true,
/** ์ปค๋ฒ๋ฆฌ์ง ๊ณต๊ธ์ ์ง์ . ๊ธฐ๋ณธ๊ฐ v8 */
provider: 'v8',
/** ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ ์ถ๋ ฅ ๋ฐฉ์ ์ง์ ('text' ์ถ๊ฐํ๋ฉด ํ
์คํธ ํ์์ผ๋ก ์ฝ์์ ์ถ๋ ฅ) */
reporter: ['text', 'html'],
/** ํ
์คํธ ์คํจ ์ ์ปค๋ฒ๋ฆฌ์ง ์์ฑ ์ฌ๋ถ */
reportOnFailure: true,
},
},
});
CI ๊ตฌ์ถ
์ง์์ ํตํฉ(CI; Continuous Integration)์ ์ฝ๋ ๋ณ๊ฒฝ ์ฌํญ์ ๋ํด ์๋ํ๋ ๋น๋์ ํ ์คํธ๋ฅผ ์ํํ ํ ๋ฉ์ธ ๋ ํฌ์งํ ๋ฆฌ์ ์ ๊ธฐ์ ์ผ๋ก ๋ณํฉํ๋ ๊ฐ๋ฐ ๋ฐฉ์์ ๊ฐ๋ฆฌํจ๋ค. ์ด๋ฅผ ํตํด ๋ฒ๊ทธ๋ฅผ ์ ์ํ๊ฒ ์ฐพ์ ํด๊ฒฐํ๊ณ , ์ํํธ์จ์ด ๋ฆด๋ฆฌ์ฆ ์ฃผ๊ธฐ๋ฅผ ๋จ์ถ์ํฌ ์ ์๋ค. ์๋ฅผ ๋ค์ด PR(Pull Request)์ ์์ฑํ๊ฑฐ๋, ๋ฉ์ธ ์ ์ฅ์์ ๋ณํฉํ ๋๋ง๋ค ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์คํํ๋๋ก ์ค์ ํด์ ์ฝ๋ ํ์ง์ ๋ณด์ฅํ ์ ์๋ค.
GitHub Actions๋ฅผ ์ด์ฉํด ์๋ก์ด PR์ด ์์ฑ๋๋ฉด ํ์ ์ฒดํฌ, Lint ์ฒดํฌ, ํ ์คํธ, ์ปค๋ฒ๋ฆฌ์ง ์์ฝ ๋ณด๊ณ ์ ์ฝ๋ฉํธ๊น์ง ์๋์ผ๋ก ๋ง๋ค์ด์ฃผ๋ ์ค์ ์ ํด๋ณด์.
ํ์ / ๋ฆฐํธ ์ฒดํฌ
๋จผ์ .github/workflows
ํด๋์ ci.yaml ํ์ผ์ ์์ฑํ๋ค. GitHub Actions ์ํฌํ๋ก์ฐ๋ yaml ํ์ผ์ ํตํด์ ์ค์ ํ ์ ์์ผ๋ฉฐ, yaml ํ์ผ์ ํญ์ workflows ํด๋์ ์์นํด ์์ด์ผ ํ๋ค.
๐ฆ Project Root
โโ .github
โโ workflows
โโ ci.yaml
๊ทธ ํ ์๋ ๋ด์ฉ์ ci.yaml ํ์ผ์ ๋ถ์ฌ ๋ฃ๋๋ค. ์ํฌํ๋ก์ฐ์ ๊ฐ ์์ (Job)์ ๋ ๋ฆฝ์ ์ผ๋ก (๋ณ๋ ฌ) ์คํ๋๊ธฐ ๋๋ฌธ์ ๋งค๋ฒ node, pnpm ์ค์น๊ฐ ํ์ํ์ง๋ง, ์บ์ ๊ธฐ๋ฅ์ ์ด์ฉํด ๊ธฐ์กด ์ค์นํ๋ ์์กด์ฑ ๋ชจ๋์ ์ฌ์ฌ์ฉํ ์ ์๋ค.
# ci.yaml
name: CI
on:
push:
branches: [main] # main ๋ธ๋์น์ pushํ๋ฉด ์ํฌํ๋ก์ฐ ์คํ
pull_request:
branches: [main] # main ๋ธ๋์น๋ก Pull Request๋ฅผ ์์ฑํ๋ฉด ์ํฌํ๋ก์ฐ ์คํ
workflow_dispatch: # ์๋ ํธ๋ฆฌ๊ฑฐ ๊ธฐ๋ฅ ํ์ฑ
jobs:
# ์ฒซ ๋ฒ์งธ Job
lint:
runs-on: ubuntu-latest
steps:
# ์ํฌํ๋ก์ฐ ์คํ ์ค ์ ์ฅ์ ์ฝ๋๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ๊ธฐ ์ํ ์ก์
- name: Checkout repository
uses: actions/checkout@v4
# pnpm ์ค์น ์ก์
. ์บ์ ์ฌ์ฉ์ ์ํด actions/setup-node@v4 ๋ณด๋ค ๋จผ์ ์ค์น
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
# Node.js๋ฅผ ์ค์นํ๊ณ pnpm ์์กด์ฑ ์บ์ ํ์ฑํ
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
# ํ๋ก์ ํธ ์์กด์ฑ ์ค์น
- name: Install dependencies
run: pnpm install
# ESLint ์คํ
- name: Run ESLint
run: pnpm lint
# ๋ ๋ฒ์งธ Job
type-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
# ํ์
์ฒดํฌ ์คํ
- name: Type check
run: pnpm type-check
๐ก yaml ํ์ผ์์ with ํค์๋๋ ์ฃผ๋ก ์ก์ ์ ์ ๋ ฅ ๋งค๊ฐ๋ณ์๋ฅผ ์ง์ ํ ๋ ์ฌ์ฉํ๋ค. with๋ uses ํค์๋ ๋ค์์ ์์นํ๋ฉฐ, with ์๋์๋ ํค-๊ฐ ์ ํํ๋ก ๋งค๊ฐ๋ณ์๋ฅผ ์ง์ ํ๋ค. with๋ก ์ง์ ๋ ์ ๋ ฅ ๋งค๊ฐ๋ณ์๋ ์ก์ ์คํ ์ ํ๊ฒฝ ๋ณ์๋ก ๋ณํ๋๋ค.
ํ ์คํธ / ์ปค๋ฒ๋ฆฌ์ง
ํ
์คํธ์ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ฒดํฌํ๊ธฐ ์ํด test-and-coverage
์ด๋ฆ์ ์์
(Job)์ ์๋์ฒ๋ผ ์ถ๊ฐํ๋ค. needs
์์ฑ์ ์์
๊ฐ ์์กด์ฑ์ ์ ์ํด์ ์คํ ์์๋ฅผ ์ ์ดํ ๋ ์ฌ์ฉํ๋๋ฐ, lint
, type-check
์์
์ ์๋ฃํ์ ๋๋ง test-and-coverage
์์
์ ์คํํ๊ธฐ ์ํด ์ถ๊ฐํ๋ค.
# ci.yaml
name: CI
on:
# ... ์๋ต
jobs:
lint:
# ... ์๋ต
type-check:
# ... ์๋ต
test-and-coverage:
# lint, type-check ์์
์ ์๋ฃํด์ผ๋ง ์คํ
needs: [lint, type-check]
runs-on: ubuntu-latest
steps:
# checkout, node/pnpm/์์กด์ฑ ์ค์น step ์ฝ๋ ์๋ต
- name: Run tests and coverage
# package.json - scripts ์์ฑ์ ์ถ๊ฐํ coverage ๋ช
๋ น์ด ์คํ
run: pnpm coverage
์ด์ PR์ ์์ฑํ๊ณ Actions ๋ฉ๋ด๋ก ๋ค์ด๊ฐ ๋ณด๋ฉด ํ ์คํธ์ ์ปค๋ฒ๋ฆฌ์ง ๊ฒ์ฌ ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ ฅํ๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
ํ ์คํธ ๋ณด๊ณ ์ ์ฝ๋ฉํธ / ์ด๋ ธํ ์ด์
publish-unit-test-result-action ์ก์ ์ ์ด์ฉํด์ ํ ์คํธ ๋ณด๊ณ ์๋ฅผ PR ์ฝ๋ฉํธ๋ก ๋จ๊ธฐ๋๋ก ์ค์ ํ ์ ์๋ค.
์ด ์ก์
์์ ์๋ฐ์คํฌ๋ฆฝํธ๋ JUnit ํ์๋ง ์ง์ํ๊ธฐ ๋๋ฌธ์ vite.config.ts ๊ตฌ์ฑ ํ์ผ์ ์๋์ฒ๋ผ ์์ ํด์ผ ํ๋ค. reporters ์์ฑ์์ default
๋ ํฐ๋ฏธ๋ ์ถ๋ ฅ ํ์์ ์๋ฏธํ๋ค. PR ํ์ด์ง Files-changed ํญ์์ ํ
์คํธ๋ฅผ ์คํจํ ์ฝ๋์ ์ด๋
ธํ
์ด์
์ ํ์ํ๋ ค๋ฉด github-actions
๋ ์ถ๊ฐํด ๋๋ค. ์ํฌํ๋ก์ฐ๊ฐ ์คํ์ค์ผ ๋ GITHUB_ACTIONS ํ๊ฒฝ ๋ณ์๊ฐ true๋ก ์ค์ ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ด์ฉํด reporters
๊ฐ์ ์กฐ๊ฑด๋ถ๋ก ์ง์ ํ๋ค.
// vite.config.ts
// ...
export default defineConfig({
// ...
test: {
// ...
/** ํ
์คํธ ์คํ ๊ฒฐ๊ณผ ๋ฆฌํฌํธ ํ์ ์ง์ */
reporters: process.env.GITHUB_ACTIONS
? ['default', 'junit', 'github-actions']
: ['default', 'junit'],
/** ํ
์คํธ ๊ฒฐ๊ณผ ํ์ผ ์ด๋ฆ ์ง์ */
outputFile: 'test-results.xml',
// ...
},
});
์ก์
์คํ์ ์ํด ci.yaml ํ์ผ test-and-coverage
์์
์ ๊ถํ(permissions)์ ์๋์ฒ๋ผ ์์ ํ๋ค.
# ci.yaml
name: CI
on:
# ...์๋ต
jobs:
lint:
# ...์๋ต
type-check:
# ...์๋ต
test-and-coverage:
needs: [lint, type-check]
runs-on: ubuntu-latest
# (์ถ๊ฐ) ๋น๊ณต๊ฐ ์ ์ฅ์๋ ์๋ 4๊ฐ ๊ถํ ํ์. ๊ณต๊ฐ ์ ์ฅ์๋ checks, pull-requests ๊ถํ ํ์
permissions:
contents: read
pull-requests: write
issues: read
checks: write
steps:
# checkout, node/pnpm/์์กด์ฑ ์ค์น step ์ฝ๋ ์๋ต
- name: Run tests and coverage
run: pnpm coverage
# (์ถ๊ฐ)
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
# ์ด์ step ์ฑ๊ณต ์ฌ๋ถ์ ์๊ด์์ด ์ด step ํญ์ ์คํ
if: always()
with:
# vite.config.ts - test.outputFile ์์ฑ์ ์ค์ ํ ํ์ผ ์ด๋ฆ
files: test-results.xml
# ๋ณด๊ณ ์ ํค๋ ์ด๋ฆ ์ง์
check_name: Unit Test Report
์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์ ์ฝ๋ฉํธ
vitest-coverage-report-action ์ก์ ์ ์ด์ฉํด์ ์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์๋ฅผ PR ์ฝ๋ฉํธ๋ก ๋จ๊ธฐ๋๋ก ์ค์ ํ ์ ์๋ค.
์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์๋ฅผ ์์ฑํ๊ธฐ ์ํด vite.config.ts ํ์ผ์ ์๋์ฒ๋ผ ์์ ํ๋ค. ์ด ์ก์
์ json-summary
๋ฆฌํฌํฐ(reporter)๊ฐ ํ์๋ก ํฌํจ๋์ด์ผ ํ๋ฉฐ, File Coverage๋ ์์ฑํ๋ ค๋ฉด json
๋ฆฌํฌํฐ๋ ์ถ๊ฐํด์ผ ํ๋ค. ์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์์ Status ํญ๋ชฉ์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋์ ๐ต ์์ผ๋ก ๋์ค๋๋ฐ, ์๊ณ๊ฐ(thresholds)์ ์ค์ ํด ๋๋ฉด ์๊ณ๊ฐ์ ์ถฉ์กฑํ์ง ์์์ ๋ ๋นจ๊ฐ์ ๐ด์์ผ๋ก ํ์ํด ์ค๋ค.
// vite.config.ts
// ...
export default defineConfig({
// ...
test: {
// ...
coverage: {
// ...
/** ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ๋ฅ ํ์ฑํ */
enabled: true,
/** ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ ์ถ๋ ฅ ๋ฐฉ์ ์ง์ */
reporter: ['text', 'html', 'json', 'json-summary'],
/** ํ
์คํธ ์คํจ ์ ์ปค๋ฒ๋ฆฌ์ง ์์ฑ ์ฌ๋ถ */
reportOnFailure: true,
/** ์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ ์ค์ */
thresholds: { lines: 70, branches: 70, functions: 70, statements: 70 },
},
},
});
๊ทธ ํ ci.yaml ํ์ผ์ Report Coverage ์คํ
์ ์ถ๊ฐํ๋ค. if: always()
์์ฑ์ด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋ ค๋ฉด(ํญ์ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ) vite.config.ts ํ์ผ์ reportOnFailure
์์ฑ์ true
๋ก ์ค์ ํด์ผ ํ๋ค.
# ci.yaml
name: CI
on:
# ...์๋ต
jobs:
lint:
# ...์๋ต
type-check:
# ...์๋ต
test-and-coverage:
needs: [lint, type-check]
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
checks: write
steps:
# checkout, node/pnpm/์์กด์ฑ ์ค์น step ์ฝ๋ ์๋ต
- name: Run tests and coverage
run: pnpm coverage
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: test-results.xml
check_name: Unit Test Report
# (์ถ๊ฐ)
- name: Report Coverage
# ์ด์ step ๊ฒฐ๊ณผ์ ์๊ด์์ด ์ด step ํญ์ ์คํ (ํญ์ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ)
# vite.config.ts ํ์ผ์์ reportOnFailure: true๋ก ์ค์ ํ์
if: always()
uses: davelosert/vitest-coverage-report-action@v2
์ต์ข ์ฝ๋
name: CI
on:
push:
branches: [main] # main ๋ธ๋์น์ push ํ๋ฉด ์ํฌํ๋ก์ฐ ์คํ
pull_request:
branches: [main] # main ๋ธ๋์น๋ก Pull Request ๋ฅผ ์์ฑํ๋ฉด ์ํฌํ๋ก์ฐ ์คํ
workflow_dispatch: # ์๋ ํธ๋ฆฌ๊ฑฐ ๊ธฐ๋ฅ ํ์ฑ
jobs:
lint:
runs-on: ubuntu-latest
steps:
# ์ํฌํ๋ก์ฐ ์คํ ์ค ์ ์ฅ์ ์ฝ๋๋ฅผ ๊ฐ์ ธ์์ ์ฌ์ฉํ๊ธฐ ์ํ ์ก์
- name: Checkout repository
uses: actions/checkout@v4
# pnpm ์ค์น ์ก์
. ์บ์ ์ฌ์ฉ์ ์ํด actions/setup-node@v4 ๋ณด๋ค ๋จผ์ ์ค์น
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
# Node.js๋ฅผ ์ค์นํ๊ณ pnpm ์์กด์ฑ ์บ์ ํ์ฑํ
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
# ํ๋ก์ ํธ ์์กด์ฑ ์ค์น
- name: Install dependencies
run: pnpm install
- name: Run ESLint
run: pnpm lint
type-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Type check
run: pnpm type-check
test-and-coverage:
# lint, type-check ์์
์ ์๋ฃํด์ผ๋ง ์คํ
needs: [lint, type-check]
runs-on: ubuntu-latest
permissions:
# publish-unit-test-result-action ์ก์
์์ private ๋ ํฌ์งํ ๋ฆฌ๋ ์๋ 4๊ฐ ๊ถํ ํ์
# public ๋ ํฌ์งํ ๋ฆฌ๋ checks, pull-requests 2๊ฐ ๊ถํ ํ์
contents: read
pull-requests: write
issues: read
checks: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run tests and coverage
run: pnpm coverage
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
# ์ด์ step ์ฑ๊ณต ์ฌ๋ถ์ ์๊ด์์ด ์ด step ํญ์ ์คํ
if: always()
with:
# vite.config.ts - test.outputFile ์์ฑ์ ์ค์ ํ ํ์ผ ์ด๋ฆ
files: test-results.xml
# ๋ณด๊ณ ์ ํค๋ ์ด๋ฆ ์ง์
check_name: Unit Test Report
- name: Report Coverage
# ์ด์ step ๊ฒฐ๊ณผ์ ์๊ด์์ด ์ด step ํญ์ ์คํ (ํญ์ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ)
# vite.config.ts ํ์ผ์์ reportOnFailure: true๋ก ์ค์ ํ์
if: always()
uses: davelosert/vitest-coverage-report-action@v2
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import svgr from 'vite-plugin-svgr';
import { coverageConfigDefaults } from 'vitest/config';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
tsconfigPaths(),
svgr({
// svgr options: https://react-svgr.com/docs/options/
svgrOptions: {
exportType: 'default',
ref: true,
svgo: false,
titleProp: true,
},
include: '**/*.svg',
}),
],
resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] },
server: { port: 3000, open: true },
test: {
/** describe, it ๋ฑ ์ ์ญ ํ
์คํธ ํจ์๋ฅผ import ์์ด ์ฌ์ฉํ ์ ์๋๋ก ์ค์ */
globals: true,
/** jsdom ํ๊ฒฝ์ ์ฌ์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์
*/
environment: 'jsdom',
/** ํ
์คํธ ์คํ ์ ํน์ ํ์ผ์ด๋ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋๋ก ์ง์ */
setupFiles: ['./src/setup-tests.ts'],
/**
* ํ
์คํธ ์คํ ๊ฒฐ๊ณผ ๋ฆฌํฌํธ ํ์ ์ง์
*
* GitHub Actions ์ํฌํ๋ก์ฐ๊ฐ ์คํ์ค์ด๋ฉด GITHUB_ACTIONS ํ๊ฒฝ ๋ณ์๋ true ๋ก ์ค์ ๋จ
* @see https://docs.github.com/ko/actions/learn-github-actions/variables#default-environment-variables
*
* publish-unit-test-result-action ์ก์
์์ ์๋ฐ์คํฌ๋ฆฝํธ๋ JUnit ๋ง ์ง์ํด์ 'junit' ์ถ๊ฐ
* 'default' ๋ ํฌํฐ๋ ํฐ๋ฏธ๋ ์ถ๋ ฅ์ ์๋ฏธ
* 'github-actions' ์ถ๊ฐํ๋ฉด PR - Files-changed ํญ์์ ์ด๋
ธํ
์ด์
ํ์ธ ๊ฐ๋ฅ
* @see https://vitest.dev/guide/reporters.html#github-actions-reporter
* */
reporters: process.env.GITHUB_ACTIONS
? ['default', 'junit', 'github-actions']
: ['default', 'junit'],
/** ํ
์คํธ ๊ฒฐ๊ณผ ํ์ผ ์ด๋ฆ ์ง์ */
outputFile: 'test-results.xml',
include: ['**/*.test.{ts,tsx,js,jsx}'],
coverage: {
/** ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ๋ฅ ํ์ฑํ */
enabled: true,
/** ์ปค๋ฒ๋ฆฌ์ง ๊ณต๊ธ์ ์ง์ . ๊ธฐ๋ณธ๊ฐ v8 */
provider: 'v8',
exclude: [
...coverageConfigDefaults.exclude,
'**/index.ts',
'**/*.config.{js,ts}',
'src/{main,app}.tsx',
],
/**
* ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ ์ถ๋ ฅ ๋ฐฉ์ ์ง์ ('text' ์ถ๊ฐํ๋ฉด ํ
์คํธ ํ์์ผ๋ก ์ฝ์์ ์ถ๋ ฅ)
* vitest-coverage-report-action ์ก์
์ฌ์ฉํ๋ ค๋ฉด 'json', 'json-summary' ์ถ๊ฐ ํ์
* @see https://github.com/davelosert/vitest-coverage-report-action#usage
* */
reporter: ['text', 'html', 'json', 'json-summary'],
/** ํ
์คํธ ์คํจ ์ ์ปค๋ฒ๋ฆฌ์ง ์์ฑ ์ฌ๋ถ */
reportOnFailure: true,
/**
* ์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ ์ค์
* @see https://github.com/davelosert/vitest-coverage-report-action#coverage-thresholds */
thresholds: {
lines: 70,
branches: 70,
functions: 70,
statements: 70,
},
},
},
});
๋ ํผ๋ฐ์ค
- Setting up a React project using Vite + TypeScript + Vitest
- About Queries | Testing Library
- ์ ๊ทผ์ฑ ํธ๋ฆฌ - MDN Web Docs ์ฉ์ด ์ฌ์ : ์น ์ฉ์ด ์ ์ | MDN
- https://github.com/EnricoMi/publish-unit-test-result-action
- https://github.com/davelosert/vitest-coverage-report-action
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Dart] ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ๋ฐ์์ ๋คํธ ํ์ต - Part 1
[Dart] ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ๋ฐ์์ ๋คํธ ํ์ต - Part 1
2024.08.21 -
[Algorithm] ํ๋ก๊ทธ๋๋จธ์ค - ํผ๋ก๋ / ๋ฐฑํธ๋ํน์ผ๋ก ๋ชจ๋ ๋ถ๋ถ์งํฉ ์ฐพ๊ธฐ
[Algorithm] ํ๋ก๊ทธ๋๋จธ์ค - ํผ๋ก๋ / ๋ฐฑํธ๋ํน์ผ๋ก ๋ชจ๋ ๋ถ๋ถ์งํฉ ์ฐพ๊ธฐ
2024.07.29 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๊ท์์ผ๋ก ์ฒ ๋จ์ ๊ตฌ๋ถ์ ์ถ๊ฐํ๊ธฐ (๋จ์ด ๊ฒฝ๊ณ, ์ ํ๋ฐฉํ์)
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๊ท์์ผ๋ก ์ฒ ๋จ์ ๊ตฌ๋ถ์ ์ถ๊ฐํ๊ธฐ (๋จ์ด ๊ฒฝ๊ณ, ์ ํ๋ฐฉํ์)
2024.07.18 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ reduce() ๋ฉ์๋ ํ์ฉ ์์ ๋ชจ์
[JS] ์๋ฐ์คํฌ๋ฆฝํธ reduce() ๋ฉ์๋ ํ์ฉ ์์ ๋ชจ์
2024.07.07