[React講座] テストとデバッグ

当ページのリンクには広告が含まれています。
目次

Jest + React Testing Library の使い方

何を組み合わせて使うのか?

フロントエンドのテストでよく出てくるのはこの3つです:

  • Jest
    ⇒ テストランナー(test(...)expect(...) を提供するやつ)
  • React Testing Library(@testing-library/react)
    ⇒ Reactコンポーネントをテスト用のDOMにレンダリングし、画面上の要素を「ユーザー目線」で触るためのユーティリティ
  • @testing-library/jest-dom
    toBeInTheDocumenttoHaveTextContent など、DOM向けの便利なアサーションを追加

最近は Vite + Vitest を使う構成も多いですが、
「Jest か Vitest か」はテストランナーの違いであって、RTLの使い方はほぼ同じです。

以降は Jest を前提に書きますが、「テストコードの形」を見る、くらいの感覚でOKです。

シンプルなコンポーネントのテスト例

テスト対象コンポーネント

// Counter.jsx
import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  )
}

テストコード(Jest + RTL)

// Counter.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'

test('ボタンを押すとカウントが増える', async () => {
  const user = userEvent.setup()

  // コンポーネントをテスト用DOMにレンダリング
  render(<Counter />)

  // 画面から要素を「ユーザー目線」で探す
  const button = screen.getByRole('button', { name: '+1' })
  const text = screen.getByText(/カウント:/)

  // 初期状態を確認
  expect(text).toHaveTextContent('カウント: 0')

  // ボタンクリック
  await user.click(button)

  // 結果の検証
  expect(text).toHaveTextContent('カウント: 1')
})

ここで大事なのは:

  • render(<Counter />)
    ⇒ コンポーネントを「テスト用の仮想DOM」に描画する
  • screen.getByRole('button', { name: '+1' })
    ⇒ 「ロール(button) + ラベル(+1)」で要素を探す
    ⇒ 実際のユーザーが見るラベルを使って探すという思想
  • userEvent.setup()user.click(...)
    ⇒ 生の fireEvent より「実際のユーザー操作に近い」イベントを発生させる
  • expect(text).toHaveTextContent('...')
    ⇒ DOMに対するアサーション(jest-dom が追加している)

stateを直接触るのではなく、「DOMにどう見えているか」を検証するのが RTL の流儀です。

非同期処理のテスト

API取得など「時間が掛かる処理」が入ったコンポーネントは、findBy 系の関数で「出てくるまで待つ」書き方をします。

const item = await screen.findByText('取得したタイトル')
expect(item).toBeInTheDocument()

また、fetch や API ラッパ関数は Jest の jest.fn() でモックしておき、

  • 成功パターン
  • 失敗パターン

をそれぞれテストすることができます。

「ユニットテスト」と「コンポーネントテスト」の違い

ユニットテスト(Unit Test)

対象:

  • 「小さな単位のロジック」
    • 単純な関数
    • カスタムフック
    • reducer

例:純粋関数

// calc.ts
export function calcTotal(price: number, taxRate: number) {
  return Math.round(price * (1 + taxRate))
}
// calc.test.ts
import { calcTotal } from './calc'

test('税込価格が正しく計算される', () => {
  expect(calcTotal(1000, 0.1)).toBe(1100)
})

例:reducer

// counterReducer.ts
export function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state + 1
    default:
      return state
  }
}
test('increment actionで1増える', () => {
  expect(counterReducer(0, { type: 'increment' })).toBe(1)
})

特徴:

  • DOMにレンダリングしない
  • 完全にロジックだけをテストする
  • テストが速く・壊れにくい

コンポーネントテスト(Component Test)

対象:

  • Reactコンポーネント(+その周辺)

例:
さきほどの CounterLoginFormPostList など。

  • render(<Component />) して
  • 「ある操作をしたら画面がこうなる」を検証する

特徴:

  • ユーザー操作と画面表示の一連の流れを確認できる
  • props 経由の連携、Context の値など、「コンポーネントの結合」も含めてテストできる
  • 内部実装を知らなくてもテストを書ける

どちらをどう使い分ける?

現実的な指針としては:

  • ビジネスロジック(計算・変換・条件分岐)
    ⇒ ユニットテストでガッチリ固める
  • UIの振る舞い(クリック⇒表示変化、フォーム⇒バリデーション表示)
    ⇒ コンポーネントテストで確認する

どちらか片方だけ、というよりは「大事なロジックはユニットで固めつつ、主要な画面はコンポーネントテストでユーザー目線チェック」というバランスが現場でも多いです。

React DevTools を使った状態確認・レンダリング確認

テストは「壊れないようにする」仕組みですが、デバッグ中に中身を直接覗くためのツールが React DevTools です。

React DevTools の基本

Chrome / Edge の拡張機能で入れると、DevTools にタブが増えます:

  • Components タブ
    ⇒ コンポーネントツリー、props・state・hooksの中身を見られる
  • Profiler タブ
    ⇒ 「どのコンポーネントが何回レンダリングされたか」を記録して分析

Components タブの使い方

  1. DevTools を開く(F12 or Ctrl+Shift+I)
  2. Components タブを開く
  3. 左側にコンポーネントツリーが表示されるので、調べたいコンポーネントをクリック
  4. 右側に
    • props
    • state
    • hooks(useState, useReducer, useContext など)
      の中身が表示される

ここでできること:

  • 「このコンポーネントが今どんな props を受け取っているか」
  • 「state がどんな値になっているか」
  • 「Context から何が渡ってきているか」

を「その時点の実物」で確認できます。

ポイント:
console.log でstateを出すより、React DevToolsで直接見るほうが「今の最新の値」が確実に分かるので、バグ調査が楽になります。

Profiler タブでレンダリング確認

パフォーマンス系のデバッグをしたいときは Profiler を使います。

  1. Profiler タブを開く
  2. 「Start profiling」ボタンを押して記録開始
  3. アプリを操作する(スクロール・クリックなど)
  4. 「Stop profiling」で記録終了
  5. どのコンポーネントが何回レンダリングされたか、どれくらい時間が掛かったかを見る

ここで、

  • 不要に何度もレンダリングされているコンポーネント
  • 明らかに重いレンダリングをしているコンポーネント

を発見して、「React.memouseMemo / useCallback を入れる目星をつける」という使い方ができます。

コンソールログだけに頼らないデバッグのやり方

もちろん console.log も有用ですが、
それだけに頼ると 「ログまみれ/どこで何が起きているか分からない」 状態になりがちです。

他の手段も組み合わせるとだいぶ楽になります。

ブラウザ DevTools のブレークポイント

  1. Chrome DevTools の Sources タブを開く
  2. 該当する .jsx / .tsx ファイルを選択
  3. 行番号をクリックしてブレークポイントを置く
  4. そのコードが実行されると、一時停止して変数の中身をじっくり見られる

ブレークポイントの利点:

  • console.log のように出力を仕込まなくても、中身が見える
  • ステップ実行(1行ずつ進める)で、「どこで値が変わったか」を追える

複雑なバグは、最初からブレークポイントで追ったほうが早いことも多いです。

ネットワークタブでAPIを確認

API周りの不具合は、JSコードを見る前に Networkタブ を見た方が早いことが多いです。

  • リクエストURLが間違っていないか
  • ステータスコード(4xx / 5xx)が返っていないか
  • レスポンスのJSONは想定どおりか

Networkタブを見て「サーバーからそもそも間違ったものが来ている」なら、フロントではなくバックエンド側の問題、という切り分けもできます。

コンソールのエラー・Warning をちゃんと読む

Reactは何かおかしいとき、コンソールにかなり親切なメッセージを出してくれます。

  • keyがないリスト
  • StrictModeによるダブルレンダリング
  • 非推奨のAPIの使用
  • メモ化ミス
    など

まずはコンソールに出ている赤いエラーや黄色いWarningをちゃんと読むこと。
それだけで7割くらいはヒントが見つかります。

小さい単位に分割して再現する

バグが見つからないときの鉄板テクニックは、「問題が起きている部分を最小限のコードに切り出して再現する」ことです。

例えば:

  • 大量のコンポーネント+Context+APIが絡んでいる画面でバグが出る
  • その部分だけを取り出して、別の小さなコンポーネントとして再実装してみる

これをやることで、「本当にバグの原因になっているコード」、「その周辺で関係ないノイズになっているコード」を切り分けやすくなります。

結果的に、「最小再現コード」を作るプロセスそのものが、一番のデバッグになっていることもよくあります。

テストを「デバッグの道具」として使う

テストは「品質保証」のためだけではなく、「バグの再現・理解のためのツール」としても使えます。

  • 再現条件をテストとして書く
  • そのテストが落ちたまま、console.log やブレークポイントでじっくり追う
  • 修正後、テストが通ることを確認し、同じバグを出さない保険にする

こうしておけば、「前にやらかしたバグ」を二度と踏まないで済みます。

まとめ

  • Jest + React Testing Library では
    • render / screen.getBy* / userEvent / expect を使って
    • 「ユーザー目線で画面がどう変わるか」をテストする
  • ユニットテスト vs コンポーネントテスト
    • ユニット:関数・reducer・カスタムフックなどロジック単体
    • コンポーネント:DOMにレンダリングしてUIの振る舞いを検証
  • React DevTools
    • Components タブ:props / state / hooks をリアルタイムに確認
    • Profiler タブ:どのコンポーネントが何回レンダリングされているか分析
  • デバッグは
    • ブレークポイント
    • Networkタブ
    • コンソールのエラー・Warning
    • 小さな再現コード
    • テストコードそのもの
      を組み合わせて、console.log だけに頼らないスタイルにしていくと楽になる

<<前へ(パフォーマンス最適化とメモ化)

>>次へ(Reactエコシステムへの入り口)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次