본문으로 건너뛰기

테스트 가이드

너홀로프로는 3-계층 테스트 전략을 씁니다.

계층도구대상실행
단위 (Unit)Vitest순수 로직, Server Action, helperpnpm test
컴포넌트 (Story)StorybookUI 상태·variant·접근성pnpm storybook
통합 (E2E)Playwright사용자 시나리오 전체pnpm e2e (Firebase 에뮬레이터 필요)
원칙

로직은 순수 함수로 분리해 mock 없이 테스트. UI 는 Storybook 에서 상태별로 시각 확인, 사용자 경로는 E2E 로 회귀 방지. 세 계층은 서로 대체가 아니라 역할 분담.

1. Vitest — 단위 테스트

위치 컨벤션

테스트는 테스트 대상과 같은 디렉토리__tests__/ 에 둡니다.

app/(workspace)/cases/_actions/
├── case-create-actions.ts
└── __tests__/
└── case-create-actions.test.ts

app/(workspace)/cases/_lib/
├── briefing-context.ts
└── __tests__/
└── briefing-context.test.ts

순수 로직은 가능한 한 _lib/ 로 분리해서 mock 없이 테스트 합니다. Firebase 나 Server Action 의존이 있는 코드는 _actions/ 쪽 테스트에서 mock 활용.

실행

pnpm test # 전체 (web + docs)
pnpm --filter=@neohollo/web test # web 만
pnpm --filter=@neohollo/web test -- <패턴> # 특정 파일
pnpm --filter=@neohollo/web test -- --watch # watch 모드

vitest.config.ts**/__tests__/**/*.test.{ts,tsx} 패턴을 수집합니다.

Firebase 모킹 패턴 (필수)

"use server" 파일을 테스트할 때는 Firebase Admin 초기화를 반드시 차단해야 합니다. 누락 시 실제 Firestore 에 붙으려 시도합니다.

import { describe, it, expect, vi, beforeEach } from "vitest";

// ① 호이스트된 mock 변수 — vi.mock 안에서 참조 가능
const { mockRequireStaffSession, mockGetServerSession } = vi.hoisted(() => ({
mockRequireStaffSession: vi.fn(),
mockGetServerSession: vi.fn(),
}));

// ② Firebase admin 초기화 차단
vi.mock("@/lib/firebase/admin", () => ({
adminDb: {
collection: vi.fn(),
doc: vi.fn(),
batch: vi.fn(),
},
}));

// ③ 세션 / 경로 유틸 모킹
vi.mock("@/lib/firebase/auth", () => ({
requireStaffSession: mockRequireStaffSession,
getServerSession: mockGetServerSession,
}));

vi.mock("@/lib/firebase/paths", () => ({
casesRef: vi.fn(),
caseRef: vi.fn(),
}));

// ④ 테스트 대상은 **절대 경로**로 직접 import
import { createCaseAction } from "@/app/(workspace)/cases/_actions/case-create-actions";

describe("createCaseAction", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("세션 없으면 인증 에러", async () => {
mockRequireStaffSession.mockRejectedValue(new Error("NO_SESSION"));
const result = await createCaseAction(new FormData());
expect(result.success).toBe(false);
});
});

Server Action 테스트 무결성 테스트

apps/web/__tests__/server-action-integrity.test.ts 가 모든 "use server" 파일이 async function 만 export 하는지, 배럴·상대경로 import 를 쓰지 않는지 자동 검증합니다. 새 Server Action 파일을 만들 때 별도 테스트 없이도 이 규칙 위반이 자동으로 걸립니다.

@neohollo/business-logic 직접 단위 테스트 (ADR 0028)

ADR 0028 로 추출된 도메인 (cases · clients · documents · evidence · execution · hearings · members · messages · portal · recoveries · refinement-feedback · snippets · tenant · case-comments) 은 wrapper 우회 직접 테스트 가능합니다. wrapper 가 아닌 mutation 자체 (Zod 검증 · Firestore write · 자동 상태 전이 등) 의 회귀 가드.

테스트 파일: apps/web/__tests__/business-logic-*.test.ts. 헬퍼: apps/web/__tests__/_helpers/fake-firestore.ts — emulator 없이 in-memory Firestore 시뮬.

import { describe, it, expect, vi } from "vitest";

vi.mock("firebase-admin/firestore", () => ({
FieldValue: {
serverTimestamp: () => "SERVER_TIMESTAMP",
delete: () => "FV_DELETE",
},
}));

import { addCaseComment } from "@neohollo/business-logic/case-comments";
import { createFakeDb } from "./_helpers/fake-firestore";

it("정상 추가 — 작성자 stamp + trim", async () => {
const fake = createFakeDb();
const out = await addCaseComment(
{
db: fake.db,
tenantId: "t-1",
actorUid: "u-1",
actorRole: "owner",
},
{ caseId: "c-1", content: " 본문 ", authorName: "Alice" },
);
expect(out.commentId).toMatch(/^auto-/);
const set = fake.writes.sets[0];
expect(set.data.content).toBe("본문"); // trim 적용
expect(set.data.authorRole).toBe("owner");
});

fakeDb 가 지원하는 API: collection · doc · get · set · update · delete · add · where('==') · count().get() · orderBy/limit (no-op) · runTransaction · batch().commit. == operator 만 지원 — 그 외 op 는 명시 throw 로 패키지 변경 시 테스트가 즉시 실패. 배포 전 회귀 가드.

dryRun ctx 옵션 검증도 같이 — dryRun: true 면 Firestore write 가 0 회여야 하고 결과 객체는 정상 반환되어야 한다는 invariant 가 모든 mutation 의 표준 테스트 항목.

커버리지 임계값

vitest.config.ts 에 디렉토리별 임계값이 선언되어 있습니다 (branches/functions/lines/statements 60% 이상):

lib/vertex-search/ · lib/rate-limit/ · lib/security/ · lib/firestore/
lib/activity/ · lib/notifications/ · lib/briefing/

해당 영역 수정 시 커버리지 떨어뜨리면 CI 실패. 새 lib 모듈은 비슷한 임계값으로 추가 권장.

pnpm --filter=@neohollo/web test -- --coverage

2. Storybook — 컴포넌트 상태

위치

UI 컴포넌트 스토리는 apps/web/stories/ 에 모여 있습니다 (약 144 stories). 파일명은 *.stories.tsx.

pnpm storybook # 포트 6006
pnpm build-storybook # 정적 빌드

작성 기준

  • 모든 shadcn/ui 래퍼 컴포넌트는 스토리 1개 이상 — variant · size · disabled 상태 커버
  • AI 결과 컴포넌트는 상태별 스토리 필수 — 로딩 · 성공 · 폴백 · 빈값 · readOnly 한도초과 (AI 파이프라인 (A) 범주)
  • 다크 모드: @storybook/addon-themes 로 자동 전환 — 별도 스토리 불필요

.storybook/

  • main.ts — addon 설정
  • preview.tsx — 글로벌 데코레이터 (테마 · i18n · mock provider)
  • mocks/ — next/navigation · next-intl · Firebase 클라이언트 모킹

3. Playwright — E2E

전제

Firebase 에뮬레이터가 localhost:5002 에 떠 있어야 합니다 (playwright.config.ts baseURL). 로컬 개발 서버(3000)가 아닌 에뮬레이터 포트입니다.

firebase emulators:start # 별도 터미널에서 먼저
pnpm e2e # 웹 빌드 → Playwright 실행

pnpm e2e 는 루트에서 정의된 대로 web 빌드 후 apps/web/e2e 디렉토리를 실행합니다. 단, Playwright 설정 자체는 루트 playwright.config.ts 에 있고 testDir: ./e2e 를 가리킵니다.

스펙 구성 (e2e/ 17개)

번호스펙커버리지
01auth로그인·회원가입·세션
02dashboard브리핑 · 퀵 액션
03case-wizard사건 생성 마법사
04screens주요 화면 스모크
05theme다크 모드 · 색상 토큰
06case-detail사건 상세 · 기일 · 증거
07doc-generateAI 서류 생성
08clients의뢰인 목록 · 대화
09settings설정 · 플랜
10staff-invite팀원 초대
11portal의뢰인 포털 (4자리 접속 코드)
12~17activity / calendar / revenue / filter / doc-edit / error나머지 주요 경로
  • fixtures/ — seed 데이터
  • helpers/ — 로그인·사건 생성 등 공통 헬퍼
  • global-setup.ts — 에뮬레이터 초기화

실행 옵션

pnpm e2e # 전체
pnpm --filter=@neohollo/web e2e -- 11-portal # 포털만
pnpm --filter=@neohollo/web e2e -- --ui # UI 모드 (디버깅)
pnpm --filter=@neohollo/web e2e -- --debug # 스텝 디버그

실패 스크린샷은 e2e/results/ 에 저장됩니다 (.gitignore 됨). retry 1회, 순차 실행(fullyParallel: false) — 테스트 간 상태 의존 때문.

CSP · 보안 헤더 회귀

apps/web/__tests__/next-config-headers.test.ts 가 CSP 헤더를 검증합니다. 새 외부 도메인을 추가하면 이 테스트가 실패하므로, apps/web/lib/security/csp-headers.tsgetCspDirectives() 와 테스트를 동시에 업데이트하세요.

4. 언제 어떤 테스트를 쓸까

상황테스트
순수 함수 (buildFallbackText, calcLoanAmounts)Vitest 단위 (mock 없음)
Server ActionVitest 단위 (Firebase mock)
UI 컴포넌트의 상태 variantStorybook
다크 모드 · 접근성Storybook addon
로그인 → 사건 생성 → 기일 등록 → 포털 확인Playwright E2E
CSP 헤더 · Server Action 무결성전용 __tests__/*-integrity.test.ts (이미 존재)

5. 테스트 작성 체크리스트

PR 올리기 전:

  • 새 Server Action → 동일 도메인 __tests__/ 에 테스트 추가 (CLAUDE.md: 200줄 이하 권장, 초과 시 분리)
  • 새 lib 순수 함수 → mock 없는 단위 테스트
  • UI 컴포넌트 신규·수정 → Storybook 스토리 추가/갱신
  • 사용자 플로우에 영향 → E2E 스펙 동기화 (e2e/)
  • pnpm type-check && pnpm lint && pnpm test 로컬 통과
  • AI 기능 수정 시 폴백 케이스 테스트 필수 (AI 파이프라인 (A) 범주는 buildFallbackText(ctx) 순수 함수 테스트로 충분)

6. 자주 발생하는 문제

  • "Firebase Admin already initialized"vi.mock("@/lib/firebase/admin") 누락. 또는 다른 테스트가 초기화한 상태가 남음 → beforeEach 에서 vi.clearAllMocks()
  • Vitest 는 통과하는데 next build 실패pnpm type-check 는 tsc 기준, next build 보다 엄격. 로컬 type-check 통과 확인
  • E2E 타임아웃 — 에뮬레이터 미기동, .firebaserc 프로젝트 매핑 확인
  • Storybook 에서 Firebase 에러.storybook/mocks/ 에 해당 모듈 mock 추가