react-testing
Testing patterns for React with Jest and React Testing Library. Use when writing tests, mocking modules, testing Zustand stores, or debugging test failures in React web applications.
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o react-testing.zip https://jpskill.com/download/17853.zip && unzip -o react-testing.zip && rm react-testing.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17853.zip -OutFile "$d\react-testing.zip"; Expand-Archive "$d\react-testing.zip" -DestinationPath $d -Force; ri "$d\react-testing.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
react-testing.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
react-testingフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
React のテスト (Web)
問題提起
React のテストには、コンポーネントのレンダリング、ユーザーインタラクション、および非同期状態管理の理解が必要です。このスキルでは、Web アプリケーション向けの Jest と React Testing Library のパターンについて説明します。
パターン: Zustand ストアのテスト
問題: ストアの状態がテスト間で永続化され、不安定なテストが発生します。
import { useAppStore } from '@/stores/appStore';
const initialState = {
items: [],
loading: false,
error: null,
};
describe('App Store', () => {
// 各テストの前にストアをリセット
beforeEach(() => {
useAppStore.setState(initialState, true); // true = 状態全体を置き換える
});
it('ストアにアイテムを追加する', async () => {
const store = useAppStore.getState();
await store.addItem({ id: '1', name: 'Test' });
expect(useAppStore.getState().items).toHaveLength(1);
});
it('ローディング状態を処理する', async () => {
const store = useAppStore.getState();
const loadPromise = store.fetchItems();
expect(useAppStore.getState().loading).toBe(true);
await loadPromise;
expect(useAppStore.getState().loading).toBe(false);
});
});
重要なポイント:
setState(initialState, true)を使用して状態を置き換える (マージしない)- 非同期操作後に
getState()で最新の状態を取得する - ストアのテストでコンポーネントの再レンダリングに依存しない
パターン: 非同期ストア操作
問題: 適切な待機を伴う非同期 Zustand アクションのテスト。
import { act, waitFor } from '@testing-library/react';
it('データを正しくロードする', async () => {
const store = useAppStore.getState();
// 非同期ストア操作を act でラップする
await act(async () => {
await store.loadData('123');
});
// 非同期完了後に状態を検証する
await waitFor(() => {
const state = useAppStore.getState();
expect(Object.keys(state.data).length).toBeGreaterThan(0);
});
});
// 複雑なフローの場合は、各ステップを検証する
it('複数ステップのフローを完了する', async () => {
const store = useAppStore.getState();
// ステップ 1
await act(async () => {
await store.loadItems();
});
expect(useAppStore.getState().items).toBeDefined();
// ステップ 2
await act(async () => {
await store.processItems();
});
expect(useAppStore.getState().processed).toBe(true);
});
パターン: コンポーネントのテスト
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('ItemCard', () => {
const mockItem = {
id: '1',
title: 'Test Item',
price: 99.99,
};
it('アイテムのデータを表示する', () => {
render(<ItemCard item={mockItem} />);
expect(screen.getByText('Test Item')).toBeInTheDocument();
expect(screen.getByText('$99.99')).toBeInTheDocument();
});
it('クリック時に onClick を呼び出す', async () => {
const user = userEvent.setup();
const onClick = jest.fn();
render(<ItemCard item={mockItem} onClick={onClick} />);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledWith(mockItem.id);
});
it('ローディング状態を表示する', () => {
render(<ItemCard item={mockItem} loading />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
パターン: React Query のテスト
問題: React Query を使用するコンポーネントには QueryClientProvider が必要です。
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
}
function renderWithQuery(ui: React.ReactElement) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
}
// テストでの使用例
it('データをフェッチして表示する', async () => {
renderWithQuery(<UserProfile userId="123" />);
// 最初はローディングを表示する
expect(screen.getByText('Loading...')).toBeInTheDocument();
// データを待機する
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
パターン: カスタムフックのテスト
import { renderHook, act, waitFor } from '@testing-library/react';
describe('useAuth', () => {
it('ユーザーをサインインする', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider, // フックがコンテキストを必要とする場合
});
await act(async () => {
await result.current.signIn('test@example.com', 'password');
});
expect(result.current.user).toBeDefined();
expect(result.current.isAuthenticated).toBe(true);
});
it('サインインエラーを処理する', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
await act(async () => {
try {
await result.current.signIn('invalid@example.com', 'wrong');
} catch (e) {
// 期待通り
}
});
expect(result.current.error).toBe('Invalid credentials');
});
});
// Zustand を使用したフック
describe('useUserData', () => {
beforeEach(() => {
useUserStore.setState(initialState, true);
});
it('現在のユーザーデータを返す', () => {
// ストアを事前に入力する
useUserStore.setState({ user: { id: '1', name: 'Test' } });
const { result } = renderHook(() => useUserData());
expect(result.current.user.name).toBe('Test');
});
});
パターン: API 呼び出しのモック
// fetch をグローバルにモックする
global.fetch = jest.fn();
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('ユーザーデータをフェッチする', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ id: '1', name: 'John' }),
});
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
// 特定のモジュールをモックする
jest.mock( 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
React Testing (Web)
Problem Statement
React testing requires understanding component rendering, user interactions, and async state management. This skill covers Jest with React Testing Library patterns for web applications.
Pattern: Zustand Store Testing
Problem: Store state persists between tests, causing flaky tests.
import { useAppStore } from '@/stores/appStore';
const initialState = {
items: [],
loading: false,
error: null,
};
describe('App Store', () => {
// Reset store before each test
beforeEach(() => {
useAppStore.setState(initialState, true); // true = replace entire state
});
it('adds item to store', async () => {
const store = useAppStore.getState();
await store.addItem({ id: '1', name: 'Test' });
expect(useAppStore.getState().items).toHaveLength(1);
});
it('handles loading state', async () => {
const store = useAppStore.getState();
const loadPromise = store.fetchItems();
expect(useAppStore.getState().loading).toBe(true);
await loadPromise;
expect(useAppStore.getState().loading).toBe(false);
});
});
Key points:
- Use
setState(initialState, true)to replace (not merge) state - Get fresh state with
getState()after async operations - Don't rely on component re-renders in store tests
Pattern: Async Store Operations
Problem: Testing async Zustand actions with proper waiting.
import { act, waitFor } from '@testing-library/react';
it('loads data correctly', async () => {
const store = useAppStore.getState();
// Wrap async store operations in act
await act(async () => {
await store.loadData('123');
});
// Verify state after async completes
await waitFor(() => {
const state = useAppStore.getState();
expect(Object.keys(state.data).length).toBeGreaterThan(0);
});
});
// For complex flows, verify each step
it('completes multi-step flow', async () => {
const store = useAppStore.getState();
// Step 1
await act(async () => {
await store.loadItems();
});
expect(useAppStore.getState().items).toBeDefined();
// Step 2
await act(async () => {
await store.processItems();
});
expect(useAppStore.getState().processed).toBe(true);
});
Pattern: Component Testing
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('ItemCard', () => {
const mockItem = {
id: '1',
title: 'Test Item',
price: 99.99,
};
it('displays item data', () => {
render(<ItemCard item={mockItem} />);
expect(screen.getByText('Test Item')).toBeInTheDocument();
expect(screen.getByText('$99.99')).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const user = userEvent.setup();
const onClick = jest.fn();
render(<ItemCard item={mockItem} onClick={onClick} />);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledWith(mockItem.id);
});
it('shows loading state', () => {
render(<ItemCard item={mockItem} loading />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
Pattern: React Query Testing
Problem: Components using React Query need QueryClientProvider.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
}
function renderWithQuery(ui: React.ReactElement) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
}
// Usage in tests
it('fetches and displays data', async () => {
renderWithQuery(<UserProfile userId="123" />);
// Shows loading initially
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
Pattern: Custom Hook Testing
import { renderHook, act, waitFor } from '@testing-library/react';
describe('useAuth', () => {
it('signs in user', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider, // If hook needs context
});
await act(async () => {
await result.current.signIn('test@example.com', 'password');
});
expect(result.current.user).toBeDefined();
expect(result.current.isAuthenticated).toBe(true);
});
it('handles sign in error', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
await act(async () => {
try {
await result.current.signIn('invalid@example.com', 'wrong');
} catch (e) {
// Expected
}
});
expect(result.current.error).toBe('Invalid credentials');
});
});
// Hook with Zustand
describe('useUserData', () => {
beforeEach(() => {
useUserStore.setState(initialState, true);
});
it('returns current user data', () => {
// Pre-populate store
useUserStore.setState({ user: { id: '1', name: 'Test' } });
const { result } = renderHook(() => useUserData());
expect(result.current.user.name).toBe('Test');
});
});
Pattern: Mocking API Calls
// Mock fetch globally
global.fetch = jest.fn();
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('fetches user data', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ id: '1', name: 'John' }),
});
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
// Mock specific module
jest.mock('@/api/users', () => ({
getUser: jest.fn(),
updateUser: jest.fn(),
}));
import { getUser, updateUser } from '@/api/users';
it('loads and updates user', async () => {
(getUser as jest.Mock).mockResolvedValue({ id: '1', name: 'John' });
(updateUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Jane' });
// Test component that uses these
});
Pattern: Router Testing
import { MemoryRouter, Routes, Route } from 'react-router-dom';
function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) {
return render(
<MemoryRouter initialEntries={[route]}>
{ui}
</MemoryRouter>
);
}
// Test navigation
it('navigates to profile on button click', async () => {
const user = userEvent.setup();
renderWithRouter(
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
);
await user.click(screen.getByText('Go to Profile'));
expect(screen.getByText('Profile Page')).toBeInTheDocument();
});
// Test with route params
it('displays user from route params', async () => {
renderWithRouter(
<Routes>
<Route path="/users/:id" element={<UserPage />} />
</Routes>,
{ route: '/users/123' }
);
await waitFor(() => {
expect(screen.getByText('User 123')).toBeInTheDocument();
});
});
Pattern: Form Testing
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('submits form with entered data', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('shows validation errors', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
// Submit without filling form
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(screen.getByText('Email is required')).toBeInTheDocument();
expect(screen.getByText('Password is required')).toBeInTheDocument();
});
it('disables submit while loading', async () => {
render(<LoginForm onSubmit={jest.fn()} loading />);
expect(screen.getByRole('button', { name: 'Sign In' })).toBeDisabled();
});
});
Pattern: Avoiding act() Warnings
Problem: "Warning: An update inside a test was not wrapped in act(...)"
// WRONG - state update happens after test
it('loads data', () => {
render(<DataComponent />);
// Component fetches data async, updates state after test ends
});
// CORRECT - wait for async completion
it('loads data', async () => {
render(<DataComponent />);
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
});
// CORRECT - use findBy* (has built-in waitFor)
it('loads data', async () => {
render(<DataComponent />);
const element = await screen.findByText('Data loaded');
expect(element).toBeInTheDocument();
});
Pattern: Snapshot Testing
When to use:
- UI components with stable structure
- Design system components
- Components where visual regression matters
When to avoid:
- Components with dynamic content
- Components that change frequently
- Large component trees (brittle)
// Good snapshot candidate - stable UI component
it('renders correctly', () => {
const { container } = render(<Button variant="primary">Submit</Button>);
expect(container).toMatchSnapshot();
});
// Bad snapshot candidate - dynamic content
it('renders user list', () => {
// Don't snapshot - list content varies
// Instead, test specific behaviors
});
Pattern: Testing Context Providers
// Create a wrapper with all providers
function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = createTestQueryClient();
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</AuthProvider>
</QueryClientProvider>
);
}
function renderWithProviders(ui: React.ReactElement) {
return render(ui, { wrapper: AllProviders });
}
// Use in tests
it('renders with all context', () => {
renderWithProviders(<Dashboard />);
// Component has access to all providers
});
Test Commands
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage report
npm test -- Button # Run specific test file
npm test -- --updateSnapshot # Update snapshots
npm test -- --runInBand # Run tests serially (debugging)
Common Issues
| Issue | Solution |
|---|---|
| "Cannot find module" | Check jest moduleNameMapper config |
| act() warning | Wrap state updates in act(), use waitFor/findBy |
| Store state bleeding | Add beforeEach with setState reset |
| Async test timeout | Increase timeout or check for hanging promises |
| Mock not working | Verify mock path matches import path exactly |
| Query not found | Use findBy* for async content, check accessibility |
Recommended File Structure
__tests__/
utils/
test-utils.tsx # Custom render with providers
query-test-utils.tsx # QueryClient wrapper
jest.setup.js # Global mocks and setup
jest.config.js # Jest configuration