본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

테스트를 돌리는 시점: 로컬, PR, CI

작성 2026. 10. 20.추가
목차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)를 보고 테스트를 보충한다.

특정 테스트 파일 빼기

도구설정주의
Vitestvitest.config.ts의 test.exclude: [...configDefaults.exclude, '**/date-format.test.tsx']configDefaults.exclude를 펼쳐 기본 제외 목록을 유지한다
Jestjest.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

원본 기록5

댓글

GitHub 계정으로 댓글을 남길 수 있어요.