🐍

Reactカスタムフックのテスト作成入門:実践編

2025/01/01に公開

はじめに

あけましておめでとうございます🎍🐍

2025年一発目の記事はテストについて記載していこうと思います✏️

こちらの記事で作成したカスタムフックのテストコードをJestReact Testing Libraryで作成していきます。(*テストのインストール作業は完了済みで進めていきます。)

昨今、テストもAIツールを使用すると土台は作成できるようになってきましたが、今回は自分自身のテストに対してのアプローチの整理も兼ねて記載していきます。

目次

以下について記載していきます。

  • なぜこのカスタムフックのテストが必要か?
  • テスト設計のアプローチ
  • 実装

なぜこのカスタムフックのテストが必要か?

Todoリストアプリケーションのカスタムフックをテストする必要性について、3つの重要な観点から説明します。

1. ビジネスロジックの信頼性確保

基本カスタムフックにはアプリの核となるロジックが集約されています。今回のTodoリストだと、タスクの追加、完了状態の管理、フィルタリングなど、アプリケーションの重要な機能が含まれているため、これらが正しく動作することを保証する必要があります。特に以下の点が重要です。

  • 状態管理の正確性: Todoの追加、削除、更新などの操作が期待通りに動作すること
  • エラー処理の確実性: 不正な入力や予期しない操作に対して適切にエラーハンドリングできること
  • エッジケースへの対応: 大量のTodo項目や特殊な入力値など、極端なケースでも正しく動作すること

2. コンポーネントからの独立したテスト

UIロジックとビジネスロジックを分離してテストすることで、より効率的で信頼性の高いテストが可能になります。

  • テストの安定性: UIの変更に影響されずにロジックをテストできるため、テストが壊れにくい
  • 保守性の向上: コンポーネントのリファクタリングがテストに影響を与えないため、メンテナンスコストが低減
  • 迅速なバグ特定: UIとロジックを分離してテストすることで、問題の原因特定が容易になる

3. 再利用可能なコードの品質担保

カスタムフックは複数のコンポーネントで共有される重要なコードです。

  • 広範な影響: 一つのバグが複数の機能に影響を及ぼす可能性があるため、入念なテストが必要
  • 使用方法の明確化: テストコードが実装の仕様書としても機能し、他の開発者が正しく使用できる

このように、カスタムフックのテストは単なる動作確認以上の価値があります。アプリケーションの品質、保守性、再利用性を高める重要な役割を果たしているのです。

テスト設計の重要性とアプローチ方法

アプリケーションのテストを実装する際、体系的なアプローチが必要です。なぜなら、単なる機能確認以上に、アプリケーションの信頼性と品質を担保する必要があるためです。

1. テスト項目の洗い出し方と具体的な実装:機能要件からのテスト設計

機能要件からテストを設計する際は、ユーザーの基本的な操作シナリオを網羅することが重要です。

基本機能(CRUD操作)
CRUDは全てのTODOアプリケーションの基礎となる機能です。これらのテストは最も優先度が高く、慎重に実装する必要があります。

    test('新しいTodoが正しく追加されること', () => {
      const { result } = renderHook(() => useTodoList())
      act(() => {
        result.current.addTodo('新しいタスク')
      })
      expect(result.current.todos).toContainEqual({
        id: expect.any(String),
        text: '新しいタスク',
        completed: false
      })
    })

このテストでは、以下の点を確認しています。

  • 新しいタスクが正しく配列に追加されるか
  • 生成されるIDの存在
  • 初期状態(completed: false)の設定

バリデーション機能
ユーザー入力の検証は、アプリケーションの堅牢性を保つために重要です。以下のテストでは、主要な検証ルールを確認します。

    test('空文字のTodoは追加されないこと', () => {
      const { result } = renderHook(() => useTodoList())
      act(() => {
        result.current.addTodo('')
      })
      expect(result.current.todos).toHaveLength(0)
      expect(result.current.error).toBe('タスクを入力してください')
    })
    

状態管理テスト
複数のTODOアイテムの状態を正しく管理できることを確認します。


test('全てのTodoを完了にした後、一つを未完了に戻せること', () => {
  const { result } = renderHook(() => useTodoList())
  act(() => {
    result.current.addTodo('タスク1')
    result.current.addTodo('タスク2')
    result.current.toggleAll(true)
    result.current.toggleTodo(result.current.todos[0].id)
  })
  expect(result.current.todos[0].completed).toBe(false)
  expect(result.current.todos[1].completed).toBe(true)
})

このテストでは以下を確認します。

  • 複数のタスクの状態を一括で変更できる
  • 個別のタスクの状態を独立して変更できる
  • 状態変更が他のタスクに影響しない

2. テスト実装の全体構造

テストの構造化は、可読性とメンテナンス性を高めるために重要です。

テストグループの構成理由

  • 関連するテストをグループ化することで、テストの目的が明確になる

  • テストの追加や修正が容易になる

  • テスト結果の分析が簡単になる

    
    describe('Todo操作', () => {
      test('追加')
      test('削除')
      test('更新')
      test('完了状態の切り替え')
    })
    
    
    • フィルタリングテスト

      
      describe('フィルタリング', () => {
        test('全件表示')
        test('完了のみ表示')
        test('未完了のみ表示')
      })
      
      
    • 状態管理

      • 単一Todo操作

        
        describe('単一Todo操作', () => {
          test('1件追加')
          test('1件削除')
          test('1件更新')
        })
        
        
      • 複数Todo操作

        
        describe('複数Todo操作', () => {
          test('複数件一括完了')
          test('複数件一括削除')
          test('フィルター切り替え時のTodo状態維持')
        })
        
        

実装例


// useTodoList.test.tsx
describe('useTodoList', () => {
  describe('Todo追加', () => {
    test('新しいTodoが正しく追加されること', () => {
      const { result } = renderHook(() => useTodoList())
      act(() => {
        result.current.addTodo('買い物に行く')
      })
      expect(result.current.todos).toHaveLength(1)
      expect(result.current.todos[0]).toEqual({
        id: expect.any(String),
        text: '買い物に行く',
        completed: false
      })
    })

    test('文字数制限を超えるTodoは追加されないこと', () => {
      const { result } = renderHook(() => useTodoList())
      const longText = 'a'.repeat(101)
      act(() => {
        result.current.addTodo(longText)
      })
      expect(result.current.todos).toHaveLength(0)
      expect(result.current.error).toBe('100文字以内で入力してください')
    })
  })

  describe('フィルタリング', () => {
    test('完了済みTodoのみが表示されること', () => {
      const { result } = renderHook(() => useTodoList())
      act(() => {
        result.current.addTodo('タスク1')
        result.current.addTodo('タスク2')
        result.current.toggleTodo(result.current.todos[0].id)
        result.current.setFilter('completed')
      })
      expect(result.current.filteredTodos).toHaveLength(1)
      expect(result.current.filteredTodos[0].completed).toBe(true)
    })
  })
})

まとめ

このような体系的なテスト設計により、以下のメリットが得られます。

  • 機能の信頼性向上
  • バグの早期発見
  • リファクタリングの安全性確保
  • コードの品質維持

適切なテスト設計と実装により、長期的なアプリケーションの保守性と品質を確保することができます。

Discussion