Tasuke HubLearn · Solve · Grow
#React

React Hooks実戦テスト戦略【2025年版】:ユニットから統合までのベストプラクティス

カスタムHooksのユニットテスト、コンポーネント統合テスト、副作用・時間依存・非同期・外部I/Oの扱い方を体系化。@testing-library/reactのrenderHook/act、偽タイマー、MSW、アクセシビリティ検証まで。

時計のアイコン13 September, 2025
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

なぜ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の品質とリグレッション耐性を両立できます。

さらに理解を深める参考書

関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。

この記事をシェア

続けて読みたい記事

編集部がピックアップした関連記事で学びを広げましょう。

#React

React Hooks超入門【2025年版】:useState/useEffectから始める実践ガイド

2025/9/13
#React

React Hooks中級【パフォーマンス最適化/2025年】:再レンダ抑制と入力応答性の実践

2025/9/13
#React

React 19の新機能 `use` フック実践ガイド【2025年版】

2025/9/19
#セキュリティ

【2025年版】AIエージェントのセキュリティテスト完全ガイド

2025/11/23
#React

React メモリリーク完全対策ガイド【2025年実務トラブルシューティング決定版】

2025/8/17
#UX

【2025年版】生成AIを使ったUXテスティング手法

2025/11/23