なぜHooksを“単体”と“統合”で分けて検証するのか
- 単体(ユニット): ロジックの境界(入力→出力)。副作用をモック/スタブに置き換え、失敗ケースも網羅。
- 統合(コンポーネント): 実DOMとイベント、フォーカス/キーボード操作、ARIAロールなどユーザー視点で検証。
両者を併走させることで、回 regressions を早期に検知しつつ、UIの実体験を担保できます。
最短で課題解決する一冊
この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。
セットアップ(推奨スタック)
- テスティング: Vitest or Jest
- DOM: @testing-library/react(render/act/userEvent)
- Hooks: @testing-library/react v14+ の
renderHook(なければ@testing-library/react-hooks) - APIモック: MSW(Mock Service Worker)
- a11y: @testing-library/jest-dom + axe-core(必要に応じて)
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
例1:純ロジックHooksのユニットテスト
// useCounter.ts
import { useState } from 'react';
export function useCounter(initial = 0) {
const [n, setN] = useState(initial);
return { n, inc: () => setN(x => x + 1), reset: () => setN(initial) };
}// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
it('increments and resets', () => {
const { result, rerender } = renderHook(({ init }) => useCounter(init), { initialProps: { init: 1 } });
expect(result.current.n).toBe(1);
act(() => result.current.inc());
expect(result.current.n).toBe(2);
act(() => result.current.reset());
expect(result.current.n).toBe(1);
rerender({ init: 10 }); // props変化
act(() => result.current.reset());
expect(result.current.n).toBe(10);
});ポイント: act でState更新をラップ。rerender で依存パラメータの変化も検証します。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
例2:非同期・時間依存Hooksのテスト
// useDebouncedValue.ts
import { useEffect, useState } from 'react';
export function useDebouncedValue<T>(value: T, delay = 300) {
const [v, setV] = useState(value);
useEffect(() => {
const t = setTimeout(() => setV(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return v;
}// useDebouncedValue.test.ts
import { renderHook, act } from '@testing-library/react';
import { vi } from 'vitest';
import { useDebouncedValue } from './useDebouncedValue';
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('debounces changes', () => {
const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value, 200), { initialProps: { value: 'a' } });
expect(result.current).toBe('a');
rerender({ value: 'ab' });
act(() => vi.advanceTimersByTime(199));
expect(result.current).toBe('a');
act(() => vi.advanceTimersByTime(1));
expect(result.current).toBe('ab');
});ポイント: 偽タイマーで時間経過を制御し、境界値(delay-1, delay)を明示的に検証します。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
例3:外部I/Oを伴うHooks(MSWでAPIモック)
// useItems.ts
export function useItems() {
const [items, setItems] = useState<any[]>([]);
useEffect(() => { fetch('/api/items').then(r => r.json()).then(setItems); }, []);
return items;
}// useItems.test.ts
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { renderHook, waitFor } from '@testing-library/react';
import { useItems } from './useItems';
const server = setupServer(rest.get('/api/items', (_req, res, ctx) => res(ctx.json([{ id: 1 }]))));
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('fetches items', async () => {
const { result } = renderHook(() => useItems());
await waitFor(() => expect(result.current).toHaveLength(1));
});ポイント: MSWで本物に近いHTTP境界を再現。waitFor で非同期完了を待ちます。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
コンポーネント統合:ユーザー操作で検証する
// CounterView.tsx
export function CounterView() {
const { n, inc } = useCounter(0);
return <button onClick={inc} aria-live="polite">{n}</button>;
}// CounterView.test.tsx
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import { CounterView } from './CounterView';
it('increments on click', async () => {
render(<CounterView />);
const btn = screen.getByRole('button');
await user.click(btn);
expect(btn).toHaveTextContent('1');
});ポイント: 役割(role)で要素を取得し、実利用に近い userEvent で操作。a11yも自然に確保できます。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
失敗しないためのチェックリスト
- Hooksの副作用はクリーンアップを実装しているか
- 偽タイマー/リアルタイマーをテスト単位で切替
- 外部I/OはMSWでモックし、エラー経路もテスト
- 統合テストではロール/ラベルで要素を取得
- 最低限のa11y検証(
toBeInTheDocument/toHaveAccessibleName等)
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
まとめ
ユニット(境界を小さく、速く)と、統合(体験を担保)の二段構えで、Hooksの品質とリグレッション耐性を両立できます。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。



