| name | write-test |
| description | 변경되었거나 테스트가 없는 Pinpoint 프론트엔드 코드에 대한 Jest 테스트를 작성합니다. 훅, 아톰, 유틸리티, 컴포넌트에 테스트가 필요하거나 커버리지를 높여야 할 때 반드시 이 스킬을 사용하세요. |
테스트 작성
$ARGUMENTS의 코드에 대한 Jest 테스트를 작성합니다 (경로가 없으면 최근 변경된 파일 대상).
1단계: 테스트가 필요한 파일 탐색
인자가 없는 경우:
git diff upstream/master --name-only
변경된 각 .ts / .tsx 파일에 대해 해당하는 .test.ts / .test.tsx 파일이 있는지 확인합니다.
테스트가 없는 파일 목록 — 이것이 우선 타겟입니다.
2단계: 코드 분석
각 타겟 파일을 완전히 읽고 파악합니다:
- 수행하는 것 (구현이 아닌 동작)
- 받는 입력 (props, 인자, 아톰 값, URL 파라미터)
- 생성하는 출력 (반환 값, 렌더링 출력, 사이드 이펙트, 아톰 업데이트)
- 의존성 (fetch 호출, 아톰, 다른 훅)
3단계: 테스트 작성
.claude/rules/testing.md의 프로젝트 테스트 패턴을 따릅니다.
규칙: 테스트 이름(describe / test / it 문자열)은 반드시 영어로 작성합니다.
유틸리티 함수 테스트
import { theUtil } from "./theUtil";
describe("theUtil", () => {
test("should return [expected behavior] given [normal input]", () => {
expect(theUtil(normalInput)).toBe(expected);
});
test("should handle null/undefined input gracefully", () => {
expect(theUtil(null)).toBe(fallback);
});
test("should handle edge cases", () => {
expect(theUtil(edgeCase)).toBe(edgeExpected);
});
});
Jotai 아톰 테스트
import { renderHook, act } from "@testing-library/react";
import { useAtom } from "jotai";
import { theAtom } from "./theAtom";
describe("theAtom", () => {
test("should initialize with default value", () => {
const { result } = renderHook(() => useAtom(theAtom));
expect(result.current[0]).toEqual(defaultValue);
});
test("should update when setter is called", () => {
const { result } = renderHook(() => useAtom(theAtom));
act(() => {
result.current[1](newValue);
});
expect(result.current[0]).toEqual(newValue);
});
});
API 훅 테스트 (useGetXxx)
import { renderHook, waitFor } from "@testing-library/react";
import { createWrapper } from "../testUtils";
import { useGetXxx } from "./useGetXxx";
describe("useGetXxx", () => {
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
test("should fetch data with correct endpoint and params", async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: mockData }),
});
const { result } = renderHook(() => useGetXxx(params), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining("/api/expected-endpoint"),
expect.any(Object),
);
expect(result.current.data).toEqual(expectedData);
});
test("should not fetch when required params are missing", () => {
const { result } = renderHook(() => useGetXxx(null), {
wrapper: createWrapper(),
});
expect(result.current.isFetching).toBe(false);
expect(global.fetch).not.toHaveBeenCalled();
});
test("should handle API error response", async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 500,
json: async () => ({ title: "Internal Server Error" }),
});
const { result } = renderHook(() => useGetXxx(params), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isError).toBe(true));
});
});
라우트 로더 테스트
import { loader } from "./loader";
describe("loader", () => {
test("should redirect with correct date format when from/to are invalid", async () => {
const request = new Request(
"http://localhost/page?from=invalid&to=invalid",
);
const response = await loader({ request, params: {} });
expect(response).toBeInstanceOf(Response);
});
test("should return null for valid date params", async () => {
const now = Date.now();
const request = new Request(
`http://localhost/page?from=${now - 3600000}&to=${now}`,
);
const response = await loader({ request, params: {} });
expect(response).toBeNull();
});
});
4단계: 커버리지 목표
각 파일에 대해 다음을 커버하는 것을 목표로 합니다:
5단계: 배치 및 등록
- 테스트 파일은 소스와 함께 위치:
myHook.ts → myHook.test.ts
- 테스트 통과 확인을 위해 실행:
yarn workspace @pinpoint-fe/ui jest path/to/file.test.ts
작성된 테스트와 통과/실패 여부를 보고합니다.