목차10개 항목
출처와 참고
web.dev의 「Learn Testing」 강좌의 「Where tests run」 모듈을 바탕으로, Vitest 명령은 공식 문서로 확인해 재구성한 노트다. 실행 옵션과 타임존 문제는 riverkim.kr에 쓴 글들에서 더했다.
세 번의 실행
flowchart TB local["[로컬] 기능을 만들며 watch 모드로 계속 실행"] -->|"push"| pr["[PR] 병합 전 상태 검사(status check). 실패하면 병합을 막는다"] pr -->|"merge"| ci["[CI] main 브랜치에서 다시 실행. 즉시 또는 정해진 주기(매시간, 매일 밤)"]
| 시점 | 목적 | 누가 보나 |
|---|---|---|
| 로컬 | 만드는 기능의 동작을 바로 확인하고 설계를 다듬는다 | 나 |
| PR | 이 변경이 기존 테스트를 깨지 않았는지 확인해 코드베이스를 초록색으로 유지한다 | 모든 기여자 (코드 호스트에 결과가 남는다) |
| main CI | 병합된 변경들이 함께 동작하는지, 프로젝트 전체 상태를 대시보드로 본다 | 팀 전체 |
PR이나 코드 리뷰가 없는 개인 프로젝트에서는 main의 CI가 마지막 안전망이다.
시작점: test 스크립트
npm·pnpm·Bun은 package.json의 test 스크립트를 특별하게 취급한다(npm test). 예외를 던지는 파일 하나(node tests.js)를 가리켜도 되지만, 보통은 테스트 러너를 가리킨다.
{
"scripts": {
"test": "vitest --run",
"test:e2e": "playwright test"
}
}- 기본
test는 몇 초 안에 끝나게 둔다. 브라우저를 띄우거나 네트워크에 나가는 테스트는 빼고 별도 스크립트로 둔다. 큰 프로젝트라면 여기에는 스모크 테스트만 넣는다. - Vitest는 기본으로
*.test.*·*.spec.*파일을 찾는다. 이름 필터(-t, --testNamePattern)나 경로 문자열(vitest user)로 일부만 돌리는 스크립트를 따로 만들 수 있다.
로컬: watch 모드와 only
| 기능 | 쓰임 |
|---|---|
| watch 모드 | 파일이 바뀌면 관련 테스트를 다시 돌린다. vitest는 개발 환경에서는 watch 모드, CI나 비대화형 터미널에서는 한 번 실행 모드로 자동 전환된다 |
test.only / describe.only | 표시한 테스트만 돌려 문제에 집중한다. 커밋 전에 지운다(린트로 막는다: JavaScript 테스트 모범 사례) |
| 파일·줄 지정 | vitest src/user.test.ts:10처럼 한 테스트만 실행한다 |
일부 파일의 커버리지만 보기
전체 커버리지는 숫자가 커서 "지금 쓴 테스트가 이 파일을 얼마나 덮는가"를 보기 어렵다. 테스트 하나만 실행하면서 원하는 소스 파일의 커버리지만 모은다. 실행하는 테스트 파일과 커버리지를 모을 파일은 달라도 된다.
# Vitest
npx vitest run user.spec.js --coverage --coverage.include=src/js/user.js
# Jest (npm 스크립트를 거칠 때는 -- 뒤 인자가 그대로 전달된다)
npm test user.spec.js -- --coverage --collectCoverageFrom=src/js/user.js결과 표의 덮이지 않은 줄 번호(Uncovered Line)를 보고 테스트를 보충한다.
특정 테스트 파일 빼기
| 도구 | 설정 | 주의 |
|---|---|---|
| Vitest | vitest.config.ts의 test.exclude: [...configDefaults.exclude, '**/date-format.test.tsx'] | configDefaults.exclude를 펼쳐 기본 제외 목록을 유지한다 |
| Jest | jest.config.js의 testPathIgnorePatterns: ['/node_modules/', 'date-format.test.tsx'] | 기본값의 /node_modules/를 덮어쓰므로 함께 적는다 |
테스트 대상 파일이 아니라 테스트 파일의 이름·경로를 적는다. 깨지는 테스트를 빼는 것은 임시방편이고, 근본 해결은 어느 환경에서도 같은 결과가 나오게 고치는 것이다(아래 플레이키 테스트).
기능을 만들면서 테스트를 같이 쓰면 그 기능이 어떻게 동작해야 하는지가 분명해진다. 이것을 순서로 정한 것이 TDD다. 작은 프로젝트에서 전체 테스트가 몇 초면 끝난다면 그냥 전부 자주 돌린다.
PR: 상태 검사
로컬에서 돌린 결과는 코드 호스트가 모른다. PR마다 테스트를 돌려 결과를 모두에게 보이게 한다. GitHub에서는 상태 검사(status check)라 부르고 GitHub Actions로 붙인다. 각 단계가 모두 성공해야 통과하고, 저장소 설정(브랜치 보호 규칙)으로 통과하지 않은 PR의 병합을 막는다. 워크플로 구성은 CI/CD에 있다.
PR은 통과했는데 CI에서 깨지는 이유
| 원인 | 설명 | 대응 |
|---|---|---|
| 변경 간 경합 | 각자 통과한 PR 여러 개가 합쳐지며 테스트되지 않은 방식으로 얽힌다 | 병합 큐(merge queue)로 합친 결과를 병합 전에 검사한다 |
| 플레이키 테스트(flaky test) | 코드가 그대로인데 통과했다 실패했다 한다. 대개 외부 시스템, 시간, 난수, 실행 순서에 기댄다. Math.random() > 0.05를 단언하면 5%는 실패한다 | 외부 의존을 가짜로 바꾸고 시간·난수를 고정한다. 재시도는 원인을 찾는 동안의 임시방편이다 |
| 환경 차이(타임존) | 날짜·시간 테스트가 특정 컴퓨터·CI에서만 실패한다. 실행 환경의 타임존에 기대고 있다 | TZ=UTC로 타임존을 고정하고, 날짜는 오프셋을 명시한 값으로 만들고, 현재 시각은 vi.setSystemTime()(Jest는 jest.useFakeTimers().setSystemTime())으로 고정한다 |
| 비싼 테스트 | E2E처럼 느린 테스트는 PR마다 돌리지 않다 보니 아무도 모르게 깨져 있다 | 주기 실행 결과를 누군가 반드시 보게 한다. 핵심 흐름만 남겨 PR에서도 돌릴 만큼 줄인다 |
빌드가 빨개졌을 때
| 대응 | 언제 |
|---|---|
| 되돌리기(roll back, revert) | 원인 변경이 분명하고 고치는 데 시간이 걸릴 때. 다른 사람의 작업을 막지 않는다 |
| 앞으로 고치기(roll forward) | 원인이 작고 바로 고칠 수 있을 때. 실패를 고치는 후속 커밋을 올린다 |
테스트는 소프트웨어를 만들기 위한 수단이지 통과 자체가 목적이 아니다. 무조건 되돌리기보다 무엇이 더 빨리 초록색으로 돌아가는지로 고른다.
경보 피로
빌드가 오래 깨져 있거나 플레이키 테스트가 자꾸 울리면 개발자는 실패 알림을 무시하게 된다(경보 피로, alarm fatigue). 결국 테스트를 꺼 버린다. 끄기 전에 다음을 먼저 한다.
- 테스트를 작게 나눈다. 범위가 좁을수록 실패 원인을 찾기 쉽다.
- 큰 E2E 하나보다 컴포넌트·통합 테스트 여러 개. 모든 것을 하는 E2E 하나보다 확신을 더 주고 유지하기 쉽다.
- 팀이 좋은 테스트를 쓰는 법을 익힌다. 플레이키 원인(대기, 공유 상태, 외부 의존)을 아는 것만으로 많이 줄어든다.
정리
- 테스트는 로컬(watch), PR(상태 검사), main CI 세 시점에 돈다.
- 기본
test스크립트는 몇 초 안에, 브라우저·네트워크 없이 끝나게 둔다. - PR에서 통과해도 경합·플레이키·비싼 테스트 때문에 CI에서 깨질 수 있다.
- 빨간 빌드는 되돌리기와 앞으로 고치기 중 더 빨리 초록으로 돌아가는 쪽을 고른다.
- 일부만 볼 때는 Vitest
--coverage.include·test.exclude(Jest--collectCoverageFrom·testPathIgnorePatterns)를 쓰되, 환경 의존 테스트는 빼지 말고 타임존·시각을 고정해 고친다. - 무시되는 테스트는 없는 것과 같다. 작고 결정적인 테스트로 경보 피로를 막는다.
관련 노트: CI/CD, JavaScript 테스트 모범 사례, 테스트 실행 환경, 프론트엔드에서의 TDD