home / skills / pluxity / pf-frontend / pf-test-hook

pf-test-hook skill

/.claude/skills/pf-test-hook

This skill generates comprehensive tests for custom hooks using Vitest and Testing Library, covering initialization, actions, prop changes, async hooks, and

npx playbooks add skill pluxity/pf-frontend --skill pf-test-hook

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.8 KB
---
name: pf-test-hook
description: 커스텀 훅 테스트 생성. "훅 테스트", "hook 테스트" 요청 시 사용.
allowed-tools: Read, Write, Glob
---

# PF 커스텀 훅 테스트 생성기

$ARGUMENTS 훅에 대한 테스트 파일을 생성합니다.

---

## 테스트 구조 (Vitest + @testing-library/react)

```tsx
import { renderHook, act, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useCounter } from "./useCounter";

describe("useCounter", () => {
  // 1. 초기 상태 테스트
  describe("초기화", () => {
    it("기본값 0으로 시작한다", () => {
      const { result } = renderHook(() => useCounter());
      expect(result.current.count).toBe(0);
    });

    it("initialValue로 시작한다", () => {
      const { result } = renderHook(() => useCounter(10));
      expect(result.current.count).toBe(10);
    });
  });

  // 2. 액션 테스트
  describe("액션", () => {
    it("increment가 count를 1 증가시킨다", () => {
      const { result } = renderHook(() => useCounter());

      act(() => {
        result.current.increment();
      });

      expect(result.current.count).toBe(1);
    });

    it("decrement가 count를 1 감소시킨다", () => {
      const { result } = renderHook(() => useCounter(5));

      act(() => {
        result.current.decrement();
      });

      expect(result.current.count).toBe(4);
    });

    it("reset이 initialValue로 되돌린다", () => {
      const { result } = renderHook(() => useCounter(10));

      act(() => {
        result.current.increment();
        result.current.increment();
        result.current.reset();
      });

      expect(result.current.count).toBe(10);
    });
  });

  // 3. props 변경 테스트
  describe("props 변경", () => {
    it("initialValue 변경 시 reset하면 새 값으로", () => {
      const { result, rerender } = renderHook(
        ({ initial }) => useCounter(initial),
        { initialProps: { initial: 0 } }
      );

      rerender({ initial: 100 });

      act(() => {
        result.current.reset();
      });

      expect(result.current.count).toBe(100);
    });
  });
});
```

---

## 비동기 훅 테스트

```tsx
import { renderHook, waitFor } from "@testing-library/react";

describe("useFetch", () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it("데이터를 성공적으로 가져온다", async () => {
    const mockData = { id: 1, name: "Test" };
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockData),
    });

    const { result } = renderHook(() => useFetch("/api/test"));

    // 로딩 상태 확인
    expect(result.current.isLoading).toBe(true);

    // 데이터 로드 대기
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBeNull();
  });

  it("에러를 처리한다", async () => {
    global.fetch = vi.fn().mockRejectedValue(new Error("Network error"));

    const { result } = renderHook(() => useFetch("/api/test"));

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.data).toBeNull();
    expect(result.current.error).toBe("Network error");
  });
});
```

---

## Zustand Store를 사용하는 훅 테스트

```tsx
import { renderHook, act } from "@testing-library/react";
import { useAuthStore } from "@/stores/auth.store";

// Store 초기화
beforeEach(() => {
  useAuthStore.setState({ user: null, isLoading: false });
});

describe("useAuth", () => {
  it("로그인 성공 시 user가 설정된다", async () => {
    const mockUser = { id: 1, name: "Test User" };
    vi.spyOn(authService, "login").mockResolvedValue(mockUser);

    const { result } = renderHook(() => useAuth());

    await act(async () => {
      await result.current.login("[email protected]", "password");
    });

    expect(result.current.user).toEqual(mockUser);
    expect(result.current.isAuthenticated).toBe(true);
  });
});
```

---

## Context Provider가 필요한 훅 테스트

```tsx
import { renderHook } from "@testing-library/react";
import { ReactNode } from "react";

const wrapper = ({ children }: { children: ReactNode }) => (
  <ThemeProvider defaultTheme="light">
    <AuthProvider>
      {children}
    </AuthProvider>
  </ThemeProvider>
);

describe("useTheme", () => {
  it("현재 테마를 반환한다", () => {
    const { result } = renderHook(() => useTheme(), { wrapper });
    expect(result.current.theme).toBe("light");
  });
});
```

---

## 테스트 체크리스트

- [ ] 초기 상태가 올바른가
- [ ] 모든 반환값이 테스트되는가
- [ ] 액션이 상태를 올바르게 변경하는가
- [ ] props/deps 변경에 반응하는가
- [ ] cleanup이 제대로 동작하는가 (useEffect)
- [ ] 에러 케이스가 처리되는가

Overview

This skill generates TypeScript test files for React custom hooks using Vitest and @testing-library/react. It produces structured tests for initialization, actions, props changes, async behavior, Zustand stores, and context-wrapped hooks, matching common patterns used by PF DEV. Use the skill by requesting "hook 테스트" or "훅 테스트" and providing the hook name and signature.

How this skill works

Given a hook name and optional details (parameters, return shape, dependencies, async behavior, store or context needs), the skill scaffolds test cases and utilities. It emits renderHook-based tests with act, waitFor, mocks for fetch or services, Zustand store resets, and a wrapper component for context providers. The output follows consistent sections: initialization, actions, props/dep changes, async flows, and cleanup checks.

When to use it

  • You need unit tests for a new or existing custom hook.
  • You want consistent, team-standard test structure for PF DEV monorepo.
  • Hook has async logic (fetches, timers) and needs mocked behaviors.
  • Hook uses global stores (Zustand) or requires context providers.
  • You want a checklist-driven test scaffold to speed PRs.

Best practices

  • Provide hook signature and example return values to generate focused assertions.
  • Include which external services to mock (fetch, authService) to create realistic tests.
  • Reset global or store state in beforeEach to avoid cross-test leaks.
  • Wrap hooks requiring context with a small wrapper that composes providers in the same order used in the app.
  • Assert initial state, all returned values, action effects, props/dep changes, and cleanup behavior explicitly.

Example use cases

  • Create tests for a counter hook: initial state, increment/decrement/reset actions, prop-based reset.
  • Generate async tests for data fetching hooks with mocked global.fetch and success/error flows.
  • Scaffold tests for hooks that consume Zustand stores and spy on service calls.
  • Produce context-wrapped tests for theme or auth hooks using a wrapper composing ThemeProvider and AuthProvider.
  • Output a test checklist to include with PRs so reviewers can verify coverage.

FAQ

What inputs do I need to provide?

Supply the hook name, parameter list and expected return shape; indicate if it uses fetch, stores, or context providers.

Can it mock network or service calls?

Yes — specify which APIs or services to mock (e.g., global.fetch, authService) so the generated tests include vi.fn() setups.