본문으로 건너뛰기

테스팅

테스트 전략 개요

Lumie 프론트엔드는 두 계층의 테스트를 운영합니다.

계층도구위치실행 명령
유닛/컴포넌트Vitest + React Testing Librarysrc/**/*.{test,spec}.{ts,tsx}npm run test:unit
E2EPlaywrighte2e/npm run test:e2e

유닛/컴포넌트 테스트 (Vitest + RTL)

vitest.config.ts 기반으로 실행됩니다. 테스트 파일은 해당 소스 파일 옆(src/entities/student/model/schema.test.ts 등)에 위치합니다. FSD 레이어 규칙의 예외로, 테스트 파일(*.test.ts(x) / *.spec.ts(x))은 계약·통합 검증을 위해 레이어 간 import를 허용합니다.

npm run test:unit # 전체 유닛 테스트

E2E 테스트 구조 (Playwright)

e2e/
├── api/ # API 테스트 (백엔드 직접 호출)
│ ├── auth/
│ │ ├── login.spec.ts
│ │ ├── register.spec.ts
│ │ └── refresh.spec.ts
│ └── exam/
│ ├── exam.spec.ts
│ └── result.spec.ts
├── flows/ # 사용자 플로우 테스트 (MonolithApiClient)
│ ├── exam-lifecycle.spec.ts
│ ├── public-pages.spec.ts
│ └── student-detail-orval.spec.ts
├── ui/ # 브라우저 UI 테스트 (Chromium)
├── ssr/ # SSR 회귀 테스트 (CSR 렌더링 검증 포함)
│ ├── auth.setup.ts # owner.json + student.json 인증 저장
│ └── ssr-regression.spec.ts
├── config/
│ ├── environments.ts
│ ├── global-setup.ts
│ └── global-teardown.ts
├── fixtures/
│ └── test-data.ts
└── utils/
├── api-client.ts # 레거시 LumieApiClient (MSA 토폴로지용)
└── monolith-client.ts # MonolithApiClient (현재 모놀리스용)

Playwright 프로젝트 구성

playwright.config.ts에 5개의 프로젝트가 정의되어 있습니다.

projects: [
{ name: 'api', testDir: './e2e/api' }, // 백엔드 API 직접 호출
{ name: 'flows', testDir: './e2e/flows' }, // 사용자 플로우
{ name: 'chromium', testDir: './e2e/ui', // 브라우저 UI (PW_UI=1 필요)
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:3000' } },
{ name: 'ssr-setup', testDir: './e2e/ssr', // 인증 한 번만 수행
testMatch: '**/auth.setup.ts' },
{ name: 'ssr', testDir: './e2e/ssr', // SSR/CSR 회귀 테스트
testMatch: '**/*.spec.ts', dependencies: ['ssr-setup'] },
]

UI/SSR 프로젝트는 실행 중인 Next.js 앱이 필요합니다. api / flows 프로젝트는 브라우저 없이 백엔드에 직접 요청합니다.

모놀리스 API 클라이언트 (MonolithApiClient)

백엔드는 단일 Spring Boot 모놀리스입니다. flows/ 테스트는 MonolithApiClient를 사용합니다.

  • 단일 베이스 URL (LUMIE_API_URL 또는 기본값 http://localhost:8080)
  • 쿠키 기반 인증(httpOnly) — Bearer 토큰 아님. Playwright APIRequestContext가 쿠키 저장소를 자동 관리
  • 테넌트 격리는 X-Tenant-Slug 헤더로 처리
  • academy 개념은 제거됨. 학생은 테넌트 단위로 범위가 지정됨
// e2e/utils/monolith-client.ts (발췌)
export class MonolithApiClient {
private context: APIRequestContext;
private baseUrl: string;

constructor() {
this.baseUrl = getMonolithBaseUrl();
}

async init(): Promise<void> {
this.context = await request.newContext({
baseURL: this.baseUrl,
extraHTTPHeaders: { 'X-Tenant-Slug': TEST_TENANT_SLUG },
});
}

async login(payload: { userLoginId: string; password: string }) {
return this.context.post('/v1/login', { data: payload });
}

async createStudent(payload: StudentPayload) {
return this.context.post('/v1/students', { data: payload });
}
}

SSR 회귀 테스트 (CSR 검증)

e2e/ssr/ssr-regression.spec.ts는 auth 라우트(admin/dashboard)가 올바르게 CSR로 동작하는지 검증합니다.

검증 항목:

  1. 클라이언트 패치 발생: 페이지 탐색 + hydration 중 브라우저에서 GET /api/v1/<resource>가 ≥1회 호출됨
  2. 데이터 렌더링: 클라이언트 패치 완료 후 엔티티 데이터 또는 빈 상태 메시지가 DOM에 존재
  3. Hydration 불일치 없음: 콘솔에 "did not match" / "Hydration failed" 경고 없음
  4. URL 상태 라운드트립: 필터/정렬/페이지 상태가 브라우저 앞뒤로 탐색 후 유지됨
  5. 미인증 안전 분기: 쿠키 없이 접근 시 충돌이나 데이터 유출 없이 로그인으로 리다이렉트

실행 방법:

TEST_CREDS_LOGIN_ID=<owner-login-id> \
TEST_CREDS_PASSWORD='<password>' \
TEST_TENANT_SLUG=demo \
BASE_URL=http://localhost:3000 \
npx playwright test --project=ssr

환경별 테스트 설정

지원 환경

환경설명
local로컬 개발 서버 (포트 기반)
dockerDocker Compose 환경
k3sKubernetes 클러스터
mirrordMirrord로 클러스터 연결
ciCI 환경 자동 감지

TEST_ENV 환경 변수로 선택합니다.

테스트 실행

# 유닛/컴포넌트 테스트 (Vitest + RTL)
npm run test:unit

# 전체 E2E 테스트
npm run test:e2e

# API 테스트만
npm run test:e2e:api

# 플로우 테스트만
npm run test:e2e:flows

# 로컬 개발 서버 대상
npm run test:e2e:local

# Kubernetes 클러스터 대상
npm run test:e2e:k3s

# Mirrord 환경
npm run test:e2e:mirrord

# HTML 리포트 확인
npm run test:e2e:report

# SSR/CSR 회귀 테스트 (실행 중인 FE + BE 필요)
npx playwright test --project=ssr

# 특정 파일만
npx playwright test e2e/api/auth/login.spec.ts

# 디버그 모드
npx playwright test --debug

테스트 데이터 팩토리

// e2e/fixtures/test-data.ts
export const TEST_TENANT_SLUG = 'test-academy';

export function generateUniqueEmail(prefix = 'test'): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}@example.com`;
}

export function createTestStudent(overrides = {}) {
return {
name: `Test Student ${Date.now()}`,
phone: '010-1234-5678',
...overrides,
};
}

export function createTestExam(overrides = {}) {
return {
name: `Test Exam ${Date.now()}`,
category: 'GRADED' as const,
totalQuestions: 5,
passScore: 60,
...overrides,
};
}

테스트 베스트 프랙티스

1. 테스트 격리

각 테스트는 새로운 API 클라이언트와 컨텍스트를 생성합니다.

test.beforeEach(async () => {
apiClient = new MonolithApiClient();
await apiClient.init();
});

test.afterEach(async () => {
await apiClient.dispose();
});

2. 유니크한 테스트 데이터

타임스탬프와 랜덤 값을 조합해 데이터 충돌을 방지합니다.

const loginId = `test-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;

3. 의미 있는 어설션

단순 응답 코드 외에 비즈니스 로직이 올바르게 동작하는지 검증합니다.

test('시험 생성', async () => {
const exam = createTestExam({ totalQuestions: 10 });
const response = await apiClient.createExam(exam);

expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.totalQuestions).toBe(10);
expect(data.status).toBe('DRAFT');
});

4. 에러 시나리오 테스트

성공 케이스뿐만 아니라 실패 케이스도 테스트합니다.

test('잘못된 자격증명으로 로그인 실패', async () => {
const response = await apiClient.login({
userLoginId: 'nonexistent',
password: 'wrongpassword',
});
expect(response.status()).toBe(401);
});