Testing 전략
Backend = JUnit5 + Testcontainers PG · Frontend = vitest + Testing Library + Playwright.
Backend (linkmusic-msa-space-was)
슬라이스 테스트 — @DataJpaTest
JPA repository · entity 검증 (인증 없이 DB 만):
@DataJpaTest
class HqEntityTest @Autowired constructor(
private val hqRepository: HqRepository,
) {
@Test
fun `INDEPENDENT 본사는 partial unique 로 중복 INSERT 차단`() {
// given Flyway 가 가상 본사 1개 INSERT
// when 같은 type INDEPENDENT 두 번째 INSERT
val dup = Hq(name = "X", type = HqType.INDEPENDENT, plan = null, billingAnchorDay = null)
// then
assertThatThrownBy { hqRepository.saveAndFlush(dup) }
.isInstanceOf(DataIntegrityViolationException::class.java)
}
}Testcontainers PostgreSQL — 실 PG 인스턴스. 인메모리 H2 사용 안 함 (PG 특이 기능 검증).
통합 테스트 — @SpringBootTest
REST endpoint · transaction · security 검증:
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureMockMvc
class AuthIntegrationTest {
@Test
fun `login 성공 시 access·refresh 발급`() {
mockMvc.post("/api/v1/auth/login") { ... }
.andExpect { status().isOk; jsonPath("$.accessToken").exists() }
}
@Test
fun `refresh 시 prev token revoked`() { ... }
@Test
fun `CORS 허용 origin 만`() { ... }
}Coverage 정책 (현재)
- 도메인 invariant · 인증 핵심 path · partial unique 같은 race condition 차단 → 반드시 테스트.
- 단순 CRUD · DTO mapping → 생략 가능.
- v1 목표 — line coverage 70% 이상 (현재 측정 X).
실행
./gradlew test # 전체
./gradlew test --tests "*AuthIntegrationTest"
./gradlew ktlintCheck test build # 풀 검증Frontend (linkmusic-frontend-space)
단위 — vitest + Testing Library
위치: *.test.ts(x) 가 같은 디렉토리.
// apps/admin/src/app/(auth)/login/login-form.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./login-form";
it("이메일 미입력 시 submit 버튼 disabled", async () => {
render(<LoginForm />);
expect(screen.getByRole("button", { name: "로그인" })).toBeDisabled();
await userEvent.type(screen.getByLabelText("이메일"), "u@x.com");
await userEvent.type(screen.getByLabelText("비밀번호"), "secret123");
expect(screen.getByRole("button", { name: "로그인" })).not.toBeDisabled();
});설정:
vitest.config.ts—environment: jsdom,setupFiles: ['./vitest.setup.ts']vitest.setup.ts—@testing-library/jest-domimporttsconfig.jsontypes —["node", "vitest/globals", "@testing-library/jest-dom"]
e2e — Playwright
위치: e2e/.
// e2e/login.spec.ts
import { test, expect } from "@playwright/test";
test("로그인 → 대시보드 진입 → /auth/me 호출", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "dev@chilloen.com");
await page.fill('[name="password"]', "...");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/");
await expect(page.locator("text=환영합니다")).toBeVisible();
});설정:
playwright.config.ts- 실행 —
pnpm test:e2e(vitest 와 분리) - 베타 단계 — e2e 는 핵심 path (login · hq-onboarding · impersonate) 만 작성
사용자 관점 검증
getByRole·getByLabelText우선 (DOM 구조 의존 X)- 접근성 확인 자동화 —
@axe-core/playwright도입 후속
실행
pnpm -r test # 모든 패키지 단위 테스트
pnpm test:e2e # apps/admin 만 e2e
pnpm test --watch # watchCI 정합
- workflow:
pnpm install --frozen-lockfilepnpm -r lint typecheck test build
- e2e 는 별도 step (선택) — 베타 단계는 manual
검증 후속 — /verify skill
워크스페이스 skill /verify:
- BE 변경 →
./gradlew ktlintCheck build test - FE 변경 →
pnpm -r lint typecheck test build - 의존성 / 타입 깨짐 사전 적발
Roadmap
- visual regression (Chromatic · Percy)
- mutation testing (PIT-Kotlin)
- coverage badge
References
- SPEC #003 §4 (AuthIntegrationTest 구조)
linkmusic-msa-space-was/build.gradle.ktslinkmusic-frontend-space/apps/admin/vitest.config.ts.claude/rules/frontend.mdtesting 섹션