[cs frontend] vitest로 된 Vitest + React Testing Library 기반 단위/통합 테스트 코드 작성
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test:playwright": "playwright test",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit:watch": "vitest",
|
||||
"test:playwright:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -54,17 +56,23 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.0.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const adminId = process.env.TEST_ADMIN_ID || "admin1gmail";
|
||||
const adminPassword = process.env.TEST_ADMIN_PW || "test1234!";
|
||||
|
||||
test.describe("로그인 페이지 E2E 테스트", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 각 테스트 시작 전 로그인 페이지로 이동
|
||||
await page.goto("/auth/login");
|
||||
});
|
||||
|
||||
test("1. 로그인 화면 UI 요소가 올바르게 렌더링되는지 확인", async ({ page }) => {
|
||||
// 메인 제목 및 하부 제목 검증
|
||||
await expect(page.locator("h1")).toContainText("유역하수도통합플랫폼");
|
||||
await expect(page.locator("h1 span")).toContainText("하수도플랫폼 통합관리(CMS)시스템");
|
||||
|
||||
// 입력 필드 및 버튼 검증
|
||||
await expect(page.locator("input#id")).toBeVisible();
|
||||
await expect(page.locator("input#password")).toBeVisible();
|
||||
await expect(page.locator("button.login_btn")).toBeVisible();
|
||||
await expect(page.locator("input#checkId")).toBeVisible();
|
||||
});
|
||||
|
||||
test("2. 빈 입력으로 로그인 시도 시 에러 메시지 검증", async ({ page }) => {
|
||||
// 값을 입력하지 않고 로그인 클릭
|
||||
await page.click("button.login_btn");
|
||||
|
||||
// 에러 메시지 영역 표시 여부 및 텍스트 검증
|
||||
const errorMessage = page.locator(".error_message");
|
||||
await expect(errorMessage).toBeVisible();
|
||||
await expect(errorMessage).not.toBeEmpty();
|
||||
});
|
||||
|
||||
test("3. 잘못된 비밀번호로 로그인 시도 시 로그인 실패 검증", async ({ page }) => {
|
||||
// 잘못된 자격 증명 입력
|
||||
await page.fill("input#id", adminId);
|
||||
await page.fill("input#password", "wrong_password_123");
|
||||
|
||||
// 로그인 시도
|
||||
await page.click("button.login_btn");
|
||||
|
||||
// 에러 메시지가 화면에 노출되는지 확인
|
||||
const errorMessage = page.locator(".error_message");
|
||||
await expect(errorMessage).toBeVisible();
|
||||
await expect(errorMessage).toContainText("아이디와 비밀번호가 일치하지 않습니다.");
|
||||
|
||||
// URL이 로그인 페이지에 그대로 머물러 있는지 확인
|
||||
await expect(page).toHaveURL(/\/auth\/login/);
|
||||
});
|
||||
|
||||
test("4. 올바른 계정 정보로 로그인 성공 시 대시보드로 이동", async ({ page }) => {
|
||||
// 올바른 자격 증명 입력
|
||||
await page.fill("input#id", adminId);
|
||||
await page.fill("input#password", adminPassword);
|
||||
|
||||
// 로그인 시도
|
||||
await page.click("button.login_btn");
|
||||
|
||||
// 대시보드 페이지로 리다이렉트 되는지 확인
|
||||
await page.waitForURL("**/dashboard");
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
|
||||
test("5. 아이디 저장 체크박스 상태 전환 확인", async ({ page }) => {
|
||||
const checkbox = page.locator("input#checkId");
|
||||
|
||||
// 기본적으로 체크되지 않은 상태인지 확인
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
|
||||
// 체크박스 클릭
|
||||
await checkbox.check();
|
||||
await expect(checkbox).toBeChecked();
|
||||
|
||||
// 체크박스 해제 클릭
|
||||
await checkbox.uncheck();
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -0,0 +1,131 @@
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { LoginPage } from "@/pages/auth/login";
|
||||
import { authService } from "@/entities/auth";
|
||||
|
||||
// next/image 모킹
|
||||
vi.mock("next/image", () => ({
|
||||
default: (props: any) => {
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img {...props} />;
|
||||
},
|
||||
}));
|
||||
|
||||
// @/entities/auth 모킹
|
||||
vi.mock("@/entities/auth", () => ({
|
||||
authService: {
|
||||
login: vi.fn(),
|
||||
},
|
||||
AuthWrapper: ({ children }: { children: React.ReactNode }) => children,
|
||||
tokenManager: {
|
||||
getToken: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// @common/assets 모킹
|
||||
vi.mock("@common/assets", () => ({
|
||||
images: {
|
||||
logo: "/logo.png",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("LoginPage 단위 테스트 (Vitest)", () => {
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeAll(() => {
|
||||
// window.location.href 변경 감지를 위해 window.location 모킹
|
||||
// @ts-ignore
|
||||
delete window.location;
|
||||
window.location = { ...originalLocation, href: "" } as any;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = originalLocation;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.location.href = "";
|
||||
});
|
||||
|
||||
it("1. 로그인 화면 UI 요소가 정상 렌더링되는지 검증", () => {
|
||||
render(<LoginPage />);
|
||||
|
||||
// 타이틀 렌더링 확인
|
||||
expect(screen.getByText("유역하수도통합플랫폼")).toBeInTheDocument();
|
||||
expect(screen.getByText("하수도플랫폼 통합관리(CMS)시스템")).toBeInTheDocument();
|
||||
|
||||
// 입력 필드 존재 확인
|
||||
const idInput = screen.getByTitle("아이디를 입력하세요.");
|
||||
const passwordInput = screen.getByTitle("비밀번호를 입력하세요.");
|
||||
expect(idInput).toBeInTheDocument();
|
||||
expect(passwordInput).toBeInTheDocument();
|
||||
|
||||
// 체크박스 존재 확인
|
||||
const saveIdCheckbox = screen.getByLabelText("아이디저장");
|
||||
expect(saveIdCheckbox).toBeInTheDocument();
|
||||
expect(saveIdCheckbox).not.toBeChecked();
|
||||
|
||||
// 로그인 버튼 존재 확인
|
||||
const submitButton = screen.getByRole("button", { name: "LOGIN" });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("2. 로그인 실패 시 에러 메시지가 화면에 노출되는지 검증", async () => {
|
||||
// authService.login이 에러를 던지도록 모크 구현
|
||||
const errorMessage = "아이디와 비밀번호가 일치하지 않습니다.";
|
||||
vi.mocked(authService.login).mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const idInput = screen.getByTitle("아이디를 입력하세요.");
|
||||
const passwordInput = screen.getByTitle("비밀번호를 입력하세요.");
|
||||
const submitButton = screen.getByRole("button", { name: "LOGIN" });
|
||||
|
||||
// 값 입력 및 제출
|
||||
fireEvent.change(idInput, { target: { value: "testuser" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "wrongpassword" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
// 에러 메시지 렌더링 대기 및 확인
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// authService.login 호출 확인
|
||||
expect(authService.login).toHaveBeenCalledWith({
|
||||
userId: "testuser",
|
||||
password: "wrongpassword",
|
||||
});
|
||||
|
||||
// 리다이렉트가 발생하지 않아야 함
|
||||
expect(window.location.href).toBe("");
|
||||
});
|
||||
|
||||
it("3. 로그인 성공 시 /dashboard 로 리다이렉트되는지 검증", async () => {
|
||||
// authService.login 성공 모크 구현
|
||||
vi.mocked(authService.login).mockResolvedValueOnce(undefined as any);
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const idInput = screen.getByTitle("아이디를 입력하세요.");
|
||||
const passwordInput = screen.getByTitle("비밀번호를 입력하세요.");
|
||||
const submitButton = screen.getByRole("button", { name: "LOGIN" });
|
||||
|
||||
// 값 입력 및 제출
|
||||
fireEvent.change(idInput, { target: { value: "admin1gmail" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "test1234!" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
// 리다이렉트 주소값 변경 대기 및 확인
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe("/dashboard");
|
||||
});
|
||||
|
||||
// authService.login 호출 확인
|
||||
expect(authService.login).toHaveBeenCalledWith({
|
||||
userId: "admin1gmail",
|
||||
password: "test1234!",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SampleCounter } from "./sample-counter";
|
||||
|
||||
describe("SampleCounter 컴포넌트 단위 및 통합 테스트", () => {
|
||||
it("1. 기본 렌더링 상태를 정상적으로 표시하는지 확인", () => {
|
||||
render(<SampleCounter />);
|
||||
|
||||
// 제목 텍스트 존재 확인
|
||||
expect(screen.getByText("단위 테스트 샘플 카운터")).toBeInTheDocument();
|
||||
|
||||
// 카운터 초기값 0 확인
|
||||
const counterVal = screen.getByTestId("counter-value");
|
||||
expect(counterVal).toHaveTextContent("현재 값: 0");
|
||||
|
||||
// 경고 문구가 보이지 않아야 함
|
||||
expect(screen.queryByTestId("warning-message")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("2. 증가 버튼 클릭 시 값이 1씩 올라가는지 확인 (상태 변경 및 이벤트 검증)", () => {
|
||||
render(<SampleCounter />);
|
||||
|
||||
const increaseBtn = screen.getByRole("button", { name: "increase" });
|
||||
const counterVal = screen.getByTestId("counter-value");
|
||||
|
||||
// 1회 클릭
|
||||
fireEvent.click(increaseBtn);
|
||||
expect(counterVal).toHaveTextContent("현재 값: 1");
|
||||
|
||||
// 2회 클릭
|
||||
fireEvent.click(increaseBtn);
|
||||
expect(counterVal).toHaveTextContent("현재 값: 2");
|
||||
});
|
||||
|
||||
it("3. 감소 버튼 클릭 시 값이 1씩 내려가는지 확인", () => {
|
||||
render(<SampleCounter />);
|
||||
|
||||
const decreaseBtn = screen.getByRole("button", { name: "decrease" });
|
||||
const counterVal = screen.getByTestId("counter-value");
|
||||
|
||||
// 1회 클릭 (0 -> -1)
|
||||
fireEvent.click(decreaseBtn);
|
||||
expect(counterVal).toHaveTextContent("현재 값: -1");
|
||||
});
|
||||
|
||||
it("4. 값이 5를 초과할 경우 경고 문구가 조건부로 렌더링되는지 확인 (통합 로직 검증)", () => {
|
||||
render(<SampleCounter />);
|
||||
|
||||
const increaseBtn = screen.getByRole("button", { name: "increase" });
|
||||
|
||||
// 5번 클릭 -> 5 (경고 없음)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
fireEvent.click(increaseBtn);
|
||||
}
|
||||
expect(screen.queryByTestId("warning-message")).not.toBeInTheDocument();
|
||||
|
||||
// 6번째 클릭 -> 6 (경고 노출되어야 함)
|
||||
fireEvent.click(increaseBtn);
|
||||
const warningMsg = screen.getByTestId("warning-message");
|
||||
expect(warningMsg).toBeInTheDocument();
|
||||
expect(warningMsg).toHaveTextContent("값이 5를 초과했습니다!");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export function SampleCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", textAlign: "center" }}>
|
||||
<h2>단위 테스트 샘플 카운터</h2>
|
||||
<p data-testid="counter-value" style={{ fontSize: "24px", fontWeight: "bold" }}>
|
||||
현재 값: {count}
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: "10px", justifyContent: "center" }}>
|
||||
<button
|
||||
onClick={() => setCount((prev) => prev - 1)}
|
||||
style={{ padding: "8px 16px" }}
|
||||
aria-label="decrease"
|
||||
>
|
||||
감소 (-)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCount((prev) => prev + 1)}
|
||||
style={{ padding: "8px 16px" }}
|
||||
aria-label="increase"
|
||||
>
|
||||
증가 (+)
|
||||
</button>
|
||||
</div>
|
||||
{count > 5 && (
|
||||
<p data-testid="warning-message" style={{ color: "red", marginTop: "10px" }}>
|
||||
값이 5를 초과했습니다!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# cs 앱 단위(Unit) 및 통합(Integration) 테스트 구축 결과 보고서
|
||||
|
||||
cs 애플리케이션 내의 컴포넌트 로직 검증 및 렌더링 검사 기능을 수행할 수 있도록 **Vitest**와 **React Testing Library (RTL)** 설정을 완료하고 샘플 코드를 생성했습니다.
|
||||
|
||||
## 변경 및 추가 사항 요약
|
||||
|
||||
### 1. 의존성 설치 및 스크립트 추가
|
||||
* `apps/cs/package.json`의 개발용 라이브러리로 다음 패키지들을 설치했습니다:
|
||||
* `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom`, `@vitejs/plugin-react`, `vite-tsconfig-paths`
|
||||
* `scripts` 항목에 단위 테스트 실행 스크립트를 정의했습니다:
|
||||
* `"test:unit": "vitest run"` (1회 실행)
|
||||
* `"test:unit:watch": "vitest"` (실시간 Watch 모드)
|
||||
|
||||
### 2. 설정 파일 정의
|
||||
* **[vitest.config.ts](file:///Users/changwng/workspace.ona/frontend/apps/cs/vitest.config.ts)**:
|
||||
* React 플러그인 및 tsconfig alias 경로 파싱을 연결했습니다.
|
||||
* 테스트 환경으로 가상 브라우저 돔(jsdom)을 연결했습니다.
|
||||
* Playwright E2E 테스트 스펙(`*.spec.ts`)과 충돌이 나지 않도록, 단위 테스트 범위를 **`tests/unit/**/*.{test,spec}.{ts,tsx}`** 경로로 제한했습니다.
|
||||
* **[setup-tests.ts](file:///Users/changwng/workspace.ona/frontend/apps/cs/tests/setup-tests.ts)**:
|
||||
* DOM 전역 단언식인 `@testing-library/jest-dom`을 확장했습니다.
|
||||
|
||||
### 3. 샘플 및 페이지 테스트 코드 생성
|
||||
* **[sample-counter.tsx](file:///Users/changwng/workspace.ona/frontend/apps/cs/tests/unit/sample-counter.tsx)**:
|
||||
* 상태값 관리(useState) 및 이벤트 처리, 5 초과 시의 경고문 조건부 렌더링 등이 구현된 React 컴포넌트입니다.
|
||||
* **[sample-counter.test.tsx](file:///Users/changwng/workspace.ona/frontend/apps/cs/tests/unit/sample-counter.test.tsx)**:
|
||||
* **기본 렌더링 검사** (초기 값 및 제목 텍스트 검증)
|
||||
* **상태 변경 검사** (증가/감소 버튼 동작에 따른 값 업데이트 검증)
|
||||
* **통합 조건부 검사** (5 초과 시의 경고 노출 여부 검증)
|
||||
* **[login-page.test.tsx](file:///Users/changwng/workspace.ona/frontend/apps/cs/tests/unit/login-page.test.tsx)**:
|
||||
* 실제 로그인 컴포넌트(`LoginPage`)를 대상으로 한 단위/통합 테스트 스위트입니다.
|
||||
* **UI 요소 검증**: 타이틀 렌더링, ID/PW 입력 필드 및 로그인 버튼 존재 확인.
|
||||
* **로그인 실패 케이스**: `authService.login` API가 에러를 발생시킬 때 화면에 한글 에러 문구(`아이디와 비밀번호가 일치하지 않습니다.`)가 정상 표출되는지 검증.
|
||||
* **로그인 성공 케이스**: 정상적으로 로그인이 완료되었을 때 `window.location.href` 가 `"/dashboard"` 로 셋업되어 이동되는지 검증.
|
||||
* *Next.js `next/image` 컴포넌트 및 글로벌 `window.location`을 가상 돔 환경에 맞추어 적절하게 모킹 처리하였습니다.*
|
||||
|
||||
---
|
||||
|
||||
## 검증 결과
|
||||
|
||||
터미널에서 `pnpm --filter cs run test:unit` 명령을 실행하여 모든 테스트(총 7개)가 정상 통과함을 검증하였습니다:
|
||||
|
||||
```bash
|
||||
RUN v4.1.10 /Users/changwng/workspace.ona/frontend/apps/cs
|
||||
|
||||
✓ tests/unit/sample-counter.test.tsx (4 tests) 89ms
|
||||
✓ tests/unit/login-page.test.tsx (3 tests) 148ms
|
||||
|
||||
Test Files 2 passed (2)
|
||||
Tests 7 passed (7)
|
||||
Start at 14:04:03
|
||||
Duration 718ms (transform 160ms, setup 71ms, import 243ms, tests 237ms, environment 557ms)
|
||||
```
|
||||
속도 면에서 아주 빠르고 안정적으로 가상 돔 유닛 테스트 환경이 준비되었습니다.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: "./tests/setup-tests.ts",
|
||||
include: ["tests/unit/**/*.{test,spec}.{ts,tsx}"],
|
||||
},
|
||||
});
|
||||
Generated
+694
-26
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user