メインコンテンツへスキップ

Reactのユニットテスト:実践的な総合ガイド

セットアップからCI/CDまで、Reactのユニットテストをマスターしましょう。Jest、RTL、Hooks、非同期code、モック、そして堅牢なクロスプラットフォームアプリのためのベストプラクティスを解説します。

Reactのユニットテスト:実践的な総合ガイド

昼食前に、ちょっとしたUIの変更をプッシュしたとします。ボタンのラベルを変えたり、条件付きレンダリングを簡略化したり、ヘルパーHookに新しい分岐を追加したり。プルリクエストは綺麗で、レビューもすぐに終わり、デプロイも完了しました。

しかし1時間後、サポートから「あるプラットフォームでログインができなくなった」と報告が入ります。Web版は問題ありません。デスクトップ版はレンダリングパスが古く、モバイル版は非同期の状態変化後に挙動が変わってしまいます。codeにはテストがあったはずなのに、誰も気づけませんでした。適切なテストではなく、テストを支える信頼できる仕組みが欠けていたからです。

これこそが、実務におけるReactのユニットテストの大きな課題です。単にテストをパスさせるのは難しくありません。リファクタリングやリリース、ホットフィックス、クロスプラットフォーム展開の際にも守ってくれるテストスイートを構築することこそが、真の難所なのです。Reactアプリが失敗するのは、チームが呼び出し方を忘れたからではありません。 render()テストが実装の詳細に寄りすぎたり、非同期の挙動が表面的なものになったり、CIがテストをリリースゲートではなく単なる「チェックボックス」として扱ったりするからこそ、アプリは失敗するのです。

モダンなReactのユニットテストは、安全装置のように機能すべきです。ローカルでの高速なフィードバック、CIでの決定論的なチェック、そしてユニットテストの範囲を明確に定義すること。これは、同じReactのコードベースがブラウザ、Capacitor コンテナ、あるいはElectronシェルを通じて配信される場合、さらに重要になります。

目次

React のユニットテストが最強のセーフティネットである理由

ユニットテストの真価は、「絶対に起こらない」と確信していたミスを検知した時に発揮されます。React においてそれは通常、コンポーネント自体はレンダリングされるものの、ユーザーが依存している挙動が変わってしまうことを意味します。無効化されていたボタンがクリック可能になる、ローディング状態が解除されない、リファクタリング後にフォールバックメッセージが消えるといったケースです。code において、これらの失敗は些細なものに見えても、本番環境では大きな代償となります。

React のテスト手法は、ある重要な転換点を経て変化しました React Testing Library が、内部実装ではなく「挙動」をテストする主流のモデルになった時ですこれにより、コンポーネントの props や state ではなく、ユーザーの挙動を模したテストへとチームがシフトしました。これは React Native のテストガイドラインである React Nativeのテスト概要にも反映されています。このシフトが重要なのは、React の code は常に再構成されるからです。Hooks が移動し、コンポーネントが分割され、Context が導入されます。内部構造に依存したテストは、健全なリファクタリングの際にも壊れてしまいますが、目に見える挙動に紐付いたテストなら、通常は耐え抜くことができます。

ユニットテストが守るべきもの

優れた React ユニットテストは、一つの小さな「契約」を守ります:

  • レンダリング結果: ユーザーに正しいテキスト、ラベル、状態、フォールバックが表示されていますか?
  • インタラクションの挙動: クリック、入力、トグル操作でUIが正しく変化しますか?
  • 境界ケースの扱い: 想定通りの入力、データ欠落、エラーパスに対してコンポーネントは正しく動作しますか?

弱いテストは、守るべきでないものを守ってしまう:

  • コンポーネントの内部実装: Stateの形状、プライベートメソッド、実装専用のprops
  • フレームワークの仕組み: Reactが内部でフックを期待通りに更新したかどうか
  • 子コンポーネントの詳細: ここでは検証する必要のない、ネストされたコンポーネントが所有するマークアップ

実践ルール: ユーザーの見た目や操作を変えずにコンポーネントをリファクタリングできるのであれば、テストも変更する必要はありません。

ユニットテストは、より広範なテストシステムの一部でもあります。アプリ全体の動作をエンドツーエンドで証明するためのものではありません。ブラウザレベルのテストやデバイスレベルの検証を行う前に、デグレを素早く検知するための高速なレイヤーです。だからこそ、これらはあらゆる合理的な プロダクションアプリ向けの自動テストスタックにおいて、.

最初の防御線となります。頻繁にリリースを行うReactチームにとって、この役割分担こそが確信を与えてくれます。ユニットテストは局所的なデグレを素早く捉え、統合テストはコンポーネント間の境界を検証し、E2Eテストはクリティカルパスを確認します。ユニット層をスキップしてしまうと、後続の低速なテストが過度な負荷を背負うことになります。

モダンなReactテスト環境の構築

脆弱なテスト環境は、アサーションを書く前からテストの不安定さ(flaky tests)を引き起こします。根本的な原因がローカル環境とCI間の設定の不一致であるにもかかわらず、多くの開発者がJestやjsdom、あるいはReactのせいにしがちです。解決策は、環境を「退屈」にすることです。この文脈において、退屈であることは良いことです。

code エディタで React のユニットテスト code を表示しているコンピューターモニターを備えた、クリーンなワークスペース。

予測可能なランナーと環境から始める

Viteなどで作成されたモダンなReactアプリの場合、ベースラインのセットアップには以下を含めるべきです:

  • テストランナー: Jestは、特にレガシーなReactコードベースやエンタープライズ向けのCIスタックにおいて、依然として一般的です。
  • ブラウザに近い環境: jsdom コンポーネントテストでDOM出力をレンダリングできます。
  • Testing Libraryのユーティリティ: @testing-library/react と @testing-library/jest-dom
  • 単一のセットアップ・エントリーポイント: matcherの登録やグローバルモックを行うための単一ファイル

Reactのテストガイドラインが推奨する主要なワークフローはシンプルです。jsdomベースの環境でコンポーネントをレンダリングし、以下のようなセレクターでUIをクエリして、 getByText または getByRoleインタラクションを発生させ、DOMの変化をアサートします。詳細は Reactのテストドキュメントに記載されています。このワークフローの信頼性を維持するには、すべてのマシンで同一のテスト環境を実行する必要があります。

実用的なJestのセットアップは、通常以下のようになります:

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

チームがBabelの代わりにSWCを使用している場合でも問題ありません。重要なのはトランスフォーマーではなく、一貫性です。一つの手法を選択し、リポジトリ内で標準化してください。JavaScriptのテストに関するより広範な慣習を知りたい場合は、Capgoの JavaScriptユニットテストガイド がチームへの引き継ぎ資料として役立ちます。

テストスイートが依存するセットアップファイルを追加する

適切な setupTests.js によって、不要な記述の繰り返しを大幅に削減できます:

import '@testing-library/jest-dom';

Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

このファイルを使えば、20個ものテストファイル内で個別に解決する代わりに、環境の差異を一度に解決できます。UIが依存するAPIのモックを追加してください。例えば、 matchMedia, ResizeObserver、または IntersectionObserverコンポーネントライブラリがそれらを必要としている場合などです。

これがないと、開発者は場当たり的にグローバル変数をパッチすることになります。その結果、テストに一貫性がなくなり、原因特定が困難な失敗が発生します。ある人のローカル環境では手動でモックを追加しているためパスするのに、CIではセットアップが共有されていないため失敗するといったことが起こります。

ローカルとCIの挙動を一致させる

ローカルのコマンドは、可能な限りCIのコマンドと一致させるべきです。開発者が緩いデフォルト設定のwatchモードを使用し、CIがより厳格な設定で実行されていると、マージ後に予期せぬ失敗が発生します。スクリプトは明示的に記述しましょう:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:ci": "jest --runInBand --coverage"
  }
}

簡単なウォークスルーがあれば、新しいチームメンバーもすぐに同じ基準で開発を開始できます:

最も影響力のあるセットアップの選択は、デフォルト設定に対する規律です。エイリアスは設定ファイルにまとめ、環境のモックは一つのセットアップファイルに集約しましょう。また、 jsdom for UI tests and a lighter environment for pure utilities when possible. The less custom behavior each individual test needs, the more reliable your system becomes.

意味のあるコンポーネントテストを書く

多くの組織が抱える問題は、テストを書けないことではありません。半年後も意味を持ち続けるテストを書けないことです。

Reactコンポーネントのユニットテストにおける標準的なパターンは、今でも正しいものです: コンポーネントをrenderし、ユーザー中心のセレクターでUIをクエリし、インタラクションをトリガーして、結果として生じるDOMの変化をassert(検証)します。、これによりテストを state や props といった実装の詳細から切り離すことができます。詳細は以下の通りです。 Reactのテストガイドコツは、そのパターンを抑制しながら適用することです。

アコーディオンは、ユーザーが実際に使うときと同じようにテストする

基本的な Accordion コンポーネントを例に見てみましょう。これはタイトル付きのボタンを描画します。パネルの内容は最初は非表示です。ボタンをクリックすると内容が表示され、アクセシビリティの状態が更新されます。

これだけで、いくつかの有用なテストが可能です:

  1. 初期レンダリングでは、タイトルは表示されるが内容は表示されない。
  2. トリガーをクリックすると内容が表示される。
  3. もう一度クリックすると内容が折りたたまれる。
  4. アクセシビリティ属性が可視状態を反映している。

最後の項目は、つい見落とされがちです。コンポーネントが aria-expanded, aria-controlsやロールベースの構造を使用している場合は、それらを検証してください。これらは実装の詳細ではなく、ユーザーに対する契約の一部です。

最高のコンポーネントテストとは、受け取りたくないようなバグレポートのように読めるものです。

意図に基づいてクエリを選択しましょう

React Testing Libraryにはいくつかのクエリスタイルがありますが、それらは互換性があるわけではありません。誤ったものを選ぶと、テストがノイズだらけになったり、誤解を招いたりします。

クエリタイプ 要素が見つかった場合 要素が見つからない場合 ユースケースの例
getBy 要素を即座に返す 即座にエラーをスローする ボタンや見出しが既に画面上にあることを検証
queryBy 要素を即座に返す を返します null 操作前に非表示コンテンツが存在しないことを検証
findBy 要素が表示された時に解決 待機後に拒否 fetchや遅延アップデート後に、非同期で読み込まれたコンテンツが表示されることを検証する

シンプルな考え方は以下の通りです:

  • 使用するのは getBy すでに存在しているはずのものに対して
  • 使用するのは queryBy まだ存在していないはずのものに対して
  • 使用するのは findBy UIが後から変化する場合に対して

もしテストが次のように始まっていたら findBy すべてに対してこれを行うのは、コンポーネントがいつ更新されるかを著者が把握できていないことを意味します。その不確実性が、後にテストの不安定さ(flakiness)につながります。

アコーディオンの実践的な例

代表的なコンポーネントは以下の通りです:

function Accordion({ title, children }) {
  const [open, setOpen] = React.useState(false);

  return (
    <section>
      <button
        aria-expanded={open}
        aria-controls="accordion-panel"
        onClick={() => setOpen(prev => !prev)}
      >
        {title}
      </button>
      {open ? (
        <div id="accordion-panel">
          {children}
        </div>
      ) : null}
    </section>
  );
}

維持すべきテストのあり方は以下の通りです:

import { render, screen, fireEvent } from '@testing-library/react';

test('renders the accordion title and hides content initially', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  expect(screen.getByRole('button', { name: /shipping details/i })).toBeInTheDocument();
  expect(screen.queryByText(/delivery takes 3 days/i)).not.toBeInTheDocument();
});

test('reveals content when the trigger is clicked', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  fireEvent.click(screen.getByRole('button', { name: /shipping details/i }));

  expect(screen.getByText(/delivery takes 3 days/i)).toBeInTheDocument();
});

test('updates aria-expanded when opened', () => {
  render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);

  const button = screen.getByRole('button', { name: /shipping details/i });
  expect(button).toHaveAttribute('aria-expanded', 'false');

  fireEvent.click(button);

  expect(button).toHaveAttribute('aria-expanded', 'true');
});

欠けている要素も同様に重要です。内部状態に対するアサーションや、 setOpen 呼び出しの確認、レンダリングツリー全体のスナップショットなどは不要です。それらは信頼性ではなく、メンテナンスコストを増やすだけです。

コンポーネントテストを強化するための習慣:

  • roleベースのクエリを優先する: ボタン、見出し、ダイアログ、アラート、入力フォームなどは、通常roleによって特定すべきです。
  • 各テストの範囲を絞る: 1つのテストにつき、ユーザーに見える振る舞いを1つに絞ることで、失敗時の原因が読みやすくなります。
  • テスト名は結果に基づいて命名する: 「正しく動作する」よりも「開いた時に aria-expanded が更新される」の方が、はるかに有用です。

DOM経由でのテストが困難なコンポーネントは、設計上の問題を示唆していることが多いです。状態の保持場所が不適切だったり、セマンティックなマークアップが不足していたりするかもしれません。優れたテストは、チームをより良いコンポーネント設計へと導きます。

カスタムHookとアプリケーションロジックのテスト

Reactアプリの重要な振る舞いの多くは、コンポーネントの外側に隠れています。状態遷移はHooksに、バリデーションやフォーマット処理はヘルパー関数に存在します。また、データの整形はレンダリング前に行われることがよくあります。目に見えるコンポーネントだけをテストしていると、本番環境の動作に影響を与える可能性のある code の大部分を見逃してしまうことになります。

HooksにはReactを意識したテスト環境が必要です

カスタムHookを正しく実行するにはReactが必要なため、次を使用してテストします: renderHook そして、状態を変更する呼び出しを次でラップします: act().

簡単な useToggle hookの例は以下の通りです:

import { useState, useCallback } from 'react';

export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(current => !current), []);
  return { value, toggle };
}

テストは公開インターフェース(public contract)に集中させるべきです:

import { renderHook, act } from '@testing-library/react';
import { useToggle } from './useToggle';

test('returns the initial value', () => {
  const { result } = renderHook(() => useToggle(true));
  expect(result.current.value).toBe(true);
});

test('toggles the value', () => {
  const { result } = renderHook(() => useToggle(false));

  act(() => {
    result.current.toggle();
  });

  expect(result.current.value).toBe(true);
});

このテストが有用な理由は、Hook自体がユニット(単位)だからです。Reactの内部動作をテストしているのではなく、Hookの外部的な振る舞いを検証しているのです。

再利用可能なUIや機能のプリミティブを構築するプロダクトチームにとって、このパターンは非常に重要です。Hookは、アプリ、デザインシステム、または社内ツール間で共有されるインターフェースになることが多いためです。商用利用を目的とした再利用可能な振る舞いを設計している場合、 開発者向けプロダクトにおける hooksに関するリソースは、Hookを単なる実装の詳細ではなく、プロダクト化されたビルディングブロックとして捉えるのに役立ちます。

テストでは純粋なロジックは純粋なまま保つべきです

すべてに jsdom、ReactやTesting Libraryが必要なわけではありません。純粋関数であれば、Node環境でJestを使ってテストすれば十分です。

例:

export function formatDisplayName(firstName: string, lastName: string) {
  return `${firstName.trim()} ${lastName.trim()}`.trim();
}

テストは極めてシンプルであるべきです:

import { formatDisplayName } from './formatDisplayName';

test('joins and trims both names', () => {
  expect(formatDisplayName(' Ada ', ' Lovelace ')).toBe('Ada Lovelace');
});

test('handles a missing last name', () => {
  expect(formatDisplayName('Ada', '')).toBe('Ada');
});

ここでの利点は、スピードと明快さです。レンダリングツリーを必要としない関数に、無理にそれを用意する必要はありません。React特有のツールを使うとオーバーヘッドが増えます。ビジネスロジックのテストは、検証対象の関数に近く、小さく、高速に保ちましょう。

実用的な切り分け方は以下の通りです:

  • Hooks: 使用するのは renderHook, act()、必要に応じてラッパーのproviderを使用します。
  • Utilities: DOMを使わず、素のJestを使用します。
  • Statefulな横断的ロジック: コンポーネントのテストが肥大化してきたら、テスト可能なヘルパー関数へ切り出しましょう。

チームは、本来もっと低レイヤーで検証すべきロジックをコンポーネントテストに詰め込みがちです。そのロジックを切り出すことで、コンポーネントテストがクリーンになり、ロジックテストの実行速度も上がるという2つのメリットが得られます。

高度なテクニックをマスターする:MockingとAsync

信頼性の低いReactテストスイートの多くは、2つの箇所で失敗します。それは依存関係の境界、そして「時間」に関連する部分です。

だからこそ、asyncテストとmockingの習熟度が、お遊びのテストスイートか、リリース前に信頼できるものかの分かれ目となります。ある分析によると、 テストの不安定さ(flakiness)の46.5%は、asyncのタイミングといった環境やリソースに起因する問題によるものです。 内の このReactユニットテストの分析では、Reactアプリにおいて、それは状態遷移、レンダリングの遅延、ネットワーク駆動のUI、そして決定論的に待機するのではなく推測で動いてしまうテストに直結しています。

高度なReactテストテクニックの比較チャート。特に「依存関係のMocking」と「非同期(Asynchronous)テスト」に焦点を当てています。

すべてのレイヤーではなく、境界をMockする

誤解を招くテストを最も早く書いてしまう方法は、コンポーネントツリーの半分をMockしてしまい、その自作Mockが正しく動くことを検証することです。

アカウントデータを取得するコンポーネントの場合、ネットワーククライアントや API モジュールをモックしてください。テストにおいてその境界での分離が真に必要でない限り、hook、子要素のrowコンポーネント、ローディングスピナー、および3つのユーティリティ関数をモックしてはいけません。

以下のルールセットに従ってください:

  • 外部サービスをモックする: HTTPクライアント、analytics、ブラウザ専用API、native bridges
  • 不安定なプラットフォームAPIをモックする: matchMediaタイマー、Electron preloadインターフェース、jsdomで利用できない Capacitor plugins
  • デフォルトでは自作の内部実装をモックしない: カスタムhook、単純な子コンポーネント、ローカルユーティリティ

難しい部分をすべてフェイクに置き換えたことでテストがパスしているなら、それはリリースへの信頼性にあまり貢献していません。

ランナーAPIの例やパターンを求めているチームには、Capgo の テストチュートリアル が実用的なリファレンスになります。特にReactは知っているがテストの仕組みにはまだ慣れていない開発者のonboardingに最適です。

タイミングが曖昧だと非同期テストは失敗する

非同期の失敗は、通常以下の3つのミスのいずれかが原因です:

  1. テストの検証(assert)が早すぎる。
  2. 任意のタイマーで待機している。
  3. コンポーネントが複数回更新されるが、テストが1回の遷移しか想定していない。

安定した非同期テストは、通常以下のような構成になります:

test('shows user details after data loads', async () => {
  render(<UserProfile userId="42" />);
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  expect(await screen.findByText(/account owner/i)).toBeInTheDocument();
});

あるいは、特定の条件を待つ必要がある場合は:

await waitFor(() => {
  expect(screen.getByRole('alert')).toBeInTheDocument();
});

使用するのは findBy 特定の要素が出現することを確認したい場合に使用します。 waitFor 条件がより広範である場合や、単一のクエリで状態を表現できない場合に使用します。 setTimeout タイマーの挙動を明示的にテストするために fake timers を使用している場合を除き、テスト内での使用は避けてください。

React のテストエコシステムでは、以下を遵守することも求められます。 act() 更新に関するセマンティクスについて。Testing Libraryが多くの部分を処理してくれますが、手動で状態を操作したりタイマーを進めたりする場合は、いつ更新が反映(flush)されるかを考慮する必要があります。

適切なモックツールの選び方

モックツールによって解決できる問題は異なります:

ツール 最適なユースケース よくある間違い
jest.fn() 独立したフェイクコールバックや注入された関数 単純なコールバックで十分な場合に、モジュール全体を置き換えてしまうこと
jest.spyOn() 実オブジェクトやモジュールの特定のメソッドを Observe または上書きする 元の実装を復元し忘れること
jest.mock() import の境界でモジュール依存関係を置き換える デフォルトで巨大なモジュールをモック化し、意味のある挙動を失ってしまうこと

具体例:

  • 〜の検証が必要なとき jest.fn() コンポーネントが〜を onSubmit 受け取るとき
  • 使用するのは jest.spyOn() 〜を確認したいとき console.errorストレージ手法、またはエクスポートされた API の呼び出しなど
  • 使用するのは jest.mock() モジュールのインポートがI/Oやネイティブの code、あるいはユニットの境界外の挙動に影響する場合

多くのガイドで不足しがちな高度な領域が、モダンReactにおけるエラーパスのテストです。Error Boundary、遅延するステート更新、非同期のフォールバックUIなどは、単なる「ハッピーパス」のクリック例だけでなく、一級のテスト対象として扱うべきです。子コンポーネントがエラーを投げたら、フォールバックUIをアサートする。リクエストが失敗したら、表示されるリカバリ状態をアサートする。ロード中にボタンが無効化されるなら、それもアサートする。これらこそが、ユーザーが実際に遭遇するバグなのです。

テストの品質と戦略の向上

多くのチームがいまだに、カバレッジを上げることが信頼性を上げることと同じだと勘違いしています。しかし、それは違います。

カバレッジの目標値を達成していても、重要なリグレッションを見逃してしまうことがあります。浅いアサーションや広すぎるスナップショット、内部実装のモックだらけのテストスイートは、安全であるかのような錯覚を与えながら、メンテナンスコストだけを増大させます。

品質の高いテストのメリットと、大量のテストスイートによるメンテナンス負荷を比較したインフォグラフィック。

カバレッジは地図であり、ゴールではない

カバレッジレポートは、「どのクリティカルパスにまだ保護がないか?」という問いに答えてくれる時にこそ価値があります。

単にパーセンテージを上げるために、些細なラッパーや静的なマークアップ、1行のパススルーファイルのテストを強いるものであれば、それは役に立ちません。カバレッジは発見ツールとして扱いましょう。認証状態、決済アクション、フィーチャーフラグ、あるいは update prompts にテストがないなら、それは警告です。一方で、表示用のアイコンコンポーネントにテストがない場合は、通常問題ありません。

健全なレビューの基準はシンプルです。「このテストはリリースリスクを軽減するか?」

  • はい: クリティカルパスにおける、ユーザーから見える振る舞いを検証しているか。
  • Yesの場合: リファクタリング時に壊れやすいビジネスロジックを保護している。
  • Noの場合: 実装の詳細をアサートしている、あるいは他のテストと価値が重複している。

ユニットテストを避けるべき対象

多くのReactガイドでは、テストすべきでない対象についての解説が不足しています。過剰なモック化や実装詳細のテストは、ユーザー体験が損なわれているにもかかわらずテストがパスしてしまう「壊れやすいテストスイート」を生むため、この差は重要です。BrowserStackのガイドでも指摘されている通り、 Reactにおけるユニットテストの対象外について.

以下のパターンは避けるか、厳格に制限してください:

  • 内部状態の検証: テストしないでください: isOpen パネルが開いたかどうかをテストできる場合は、直接的な検証は避けます。
  • フレームワークの挙動: ReactがEffectを呼び出したかどうかをテストするのではなく、そのEffectによって何が変化したかという結果をテストしてください。
  • サードパーティライブラリの内部実装: ライブラリ自体のレンダリングロジックではなく、DatePickerやRouterとの連携部分をテストしてください。
  • 細切れすぎるユニット: すべての子コンポーネントとヘルパーをモックしてしまうと、意味のある振る舞いをテストできていないかもしれません。

悪いテストは、リファクタリングを妨げたうえに本番バグも検出できないなら、テストがないより悪いです。

有用なヒューリスティックは「境界の所有権」です。code が所有する範囲をテストしましょう。React、ブラウザ、成熟したライブラリがすでに担っている部分は、統合レイヤーが契約を変えない限りテストしないでください。

スナップショットが役立つ場面と害になる場面

スナップショットは無用ではありません。ただ、誤用しやすいだけです。

出力が安定してシンプルで、広範な構造差分に意味があるコンポーネントには控えめに使いましょう。インタラクティブなコンポーネントや変化の激しいコンポーネントでは、ノイズになるので避けましょう。開発者は内容を読まなくなり、反射的に更新するようになります。

通常は、より良い代替手段があります:

  • 条件付きレンダリングでは、キーとなるテキストの有無を検証します。
  • 視覚的な状態変化では、重要な role、label、属性を検証します。
  • エラーやフォールバックでは、実際のメッセージやアラート領域を検証します。

ユニットテストを超えた、より広範な品質プロセスが必要なら、頼れる相棒は アプリの品質保証(QA)ワークフロー テスト、リリースチェック、ロールバック計画を一つのシステムとして捉えること。これこそが、テストの品質を最も速く向上させるマインドセットの転換です。「テストがいくつあるか」ではなく、「どの失敗がユーザーに届いてしまう可能性があるか」を問い始めてください。

クロスプラットフォームのCI/CDパイプラインへのテスト統合

開発者のローカル環境でしか実行されないテストスイートは、単なる「提案」であって「制御」ではありません。

すべてのプルリクエストがクリーンな環境で同じチェックを実行し、チェックに失敗した際にマージをブロックして初めて、テストスイートは実用的なものになります。当たり前のことのように聞こえますが、多くのチームがいまだに重大な欠落を抱えています。テストが手動で実行されていたり、カバレッジレポートが任意だったり、テストジョブの完了前にパッケージングやリリースジョブが始まっていたりします。こうした隙が、小さなUIの退行(リグレッション)を、リリース全体の致命的な失敗へと繋げてしまうのです。

Reactの自動テストをCI/CD開発パイプラインに統合するプロセスを示す5ステップのフローチャート。

プルリクエストは毎回、常に同じゲートを通過すべきである

Reactのユニットテストをセーフティネットとして機能させるには、CIに以下の要素が不可欠です:

  • すべてのプルリクエストで実行する
  • lockfileから依存関係をインストールする
  • 常に同じテストコマンドを使用する
  • テスト失敗時は即座に(Fail fast)停止する
  • テスト合格後にのみアーティファクトを公開する

これは〜の核心です: アプリ開発チームにおける継続的なデプロイメントの実践リリース後ではなく、リリース前に確信を得るために。

多くのチームにとって、シンプルな GitHub Actionsのワークフローで十分です:

name: test

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  react-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test:ci

これは凝ったものではありませんが、それが重要です。最も強力なパイプラインとは、通常、最も予測可能なものです。

Capacitor と Electron でこれがより重要になる理由

クロスプラットフォームの React アプリは、ブラウザ専用アプリよりもリリースリスクが高くなります。なぜなら、同じ UI code が、異なるランタイムの前提を持つ異なるコンテナで配信されることが多いからです。

パイプラインが役立つ例をいくつか挙げます:

  • Capacitor アプリ: Web code はローカルではパスしても、パッケージ化後にプラグインブリッジ、オフライン状態、またはアプリのライフサイクルのエッジケースによって挙動が変わると失敗することがあります。
  • Electron アプリ: レンダラーコンポーネントが、preload API、window メッセージング、またはデスクトップ専用の状態に依存している場合、意図的にモック化しない限り、通常のブラウザテストでは存在しません。
  • リリース・トレインの共有: デプロイプロセスで公開を厳密に制御していない場合、一つの不具合のあるbundleが複数のターゲットに影響を及ぼす可能性があります。

そのため、ユニットテストはパッケージング・ジョブの前に、パッケージング・ジョブは配布ジョブの前に実行されるべきです。各ステージでリスクを最小限に抑えます。ユニットテストはローカルのデグレを迅速に検出し、プラットフォームのパッケージングは環境の前提条件を検証します。そして、手動承認や段階的なロールアウトによって、最終的なリリースの信頼性を確保します。

実践的な GitHub Actionsワークフロー

より成熟したパイプラインでは、通常、責任範囲を以下のように分割します:

  1. Testジョブ: 高速なユニットテストおよびhookテスト
  2. Buildジョブ: テスト合格後のみプロダクションビルドを実行
  3. Packageジョブ: Capacitor sync、Electronパッケージング、またはartifactのバンドル
  4. Releaseジョブ: 承認済みのブランチまたはタグからのみ公開する

Capacitor や Electron アプリに Live Update を配信しているチームにとって、リリースツールの重要性は非常に高いです。そのワークフローにおける一つの選択肢が Capgoです。これは、ロールバック機能とチャンネルベースのロールアウト制御を備えた、CapacitorJS および Electron アプリ向けの署名済み Web bundle を公開します。実務的には、React のテストジョブを、Web bundle が staging や production へ昇格する前の「最初の厳格なゲート」として機能させることができます。

運用ルールはシンプルです。テストの脆弱さをリリースインフラで補おうとしないでください。リリースインフラは、信頼できるテストによって不適切な変更がすでに排除された後に使用するものです。

信頼できるテストシステムは、チームの行動を変えます。エンジニアは迷いなくマージできるようになり、レビュアーは基本的な動作の再確認ではなく、エッジケースに集中できるようになります。リリースマネージャーも、すべてのデプロイをギャンブルのように扱うことはなくなります。これこそが、React のユニットテストを適切に行うことで得られる成果です。


Capacitor や Electron を通じて React を配信している場合、リリースの安全性はローカルでのテスト通過(green tests)だけで決まるものではありません。 Capgo は、署名済みの Web update を公開し、ターゲットとなるロールアウトチャンネルを指定し、ストアの審査を待たずに問題のある bundle をロールバックするための制御された手段をチームに提供します。これは、デプロイ前にユニットテストの通過を必須とする CI pipeline の後続プロセスとして、自然に組み込むことができます。

Capacitor アプリ向けの Live Update

Webレイヤーのバグが本番に出たときは、アプリストアの審査を何日も待たずに Capgo で修正を配信できます。ネイティブの変更は通常の審査フローに乗せたまま、ユーザーにはバックグラウンドでアップデートが届きます。

最新のブログ記事

Capgo は、本格的なプロフェッショナルモバイルアプリの開発に必要な最高のインサイトを提供します。