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.tsenvironment: jsdom, setupFiles: ['./vitest.setup.ts']
  • vitest.setup.ts@testing-library/jest-dom import
  • tsconfig.json types — ["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      # watch

CI 정합

  • workflow:
    • pnpm install --frozen-lockfile
    • pnpm -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.kts
  • linkmusic-frontend-space/apps/admin/vitest.config.ts
  • .claude/rules/frontend.md testing 섹션