昼食前に小さなUI変更をプッシュします。一見、無害に見えます。ボタンのラベルが変わり、条件付きレンダリングが簡略化され、ヘルパーフックに新しい分岐が1つ増えるだけ。プルリクエストはクリーンで、レビューもすぐ終わり、デプロイされます。
1時間後、サポートから、あるプラットフォームでログインが動かなくなったと報告が入ります。Webは問題なさそうです。デスクトップシェルはレンダリングパスが古く、モバイルビルドは非同期の状態変化後に挙動が異なります。誰も気づかなかったのは、code にテストはあっても、適切なテストではなく、テストを支える信頼できる仕組みもなかったからです。
これが、プロダクションチームでReactのユニットテストを行う際の主な問題です。パスするテストをいくつか書くのは難しくありません。リファクタリング、リリーストレイン、ホットフィックス、クロスプラットフォームのパッケージングの間も守ってくれるスイートを構築するのが難しいところです。Reactアプリが失敗するのは、チームが呼び出し方を忘れたからではありません。 render()失敗するのは、テストが実装の詳細に寄り、非同期の挙動が覆い隠され、CIがテストをリリースゲートではなくチェックボックスとして扱うからです。
現代のReactユニットテストは、セーフティシステムとして機能するときに真価を発揮します。ローカルでの迅速なフィードバック、CIでの決定論的なチェック、そしてユニットテストの範囲と範囲外の明確な境界線。同じReactコードベースがブラウザ、Capacitor コンテナ、またはElectronシェルを通じてリリースされる場合、その重要性はさらに高まります。
目次
- React のユニットテストが最強のセーフティネットである理由
- モダンなReactテスト環境の構築方法
- 意味のあるコンポーネントテストを書く
- カスタムHookとアプリケーションロジックのテスト
- 高度なテクニック:MockとAsyncをマスターする
- テストの品質と戦略を向上させる
- クロスプラットフォームCI/CDパイプラインへのテスト統合
React のユニットテストが最強のセーフティネットである理由
「絶対に起こらない」と確信していたミスをユニットテストが検知したとき、その価値が証明されます。React では通常、コンポーネント自体はレンダリングされるのに、ユーザーが依存している挙動が変わってしまうことを意味します。無効化されていたボタンがクリックできるようになる、ローディング状態が解除されない、リファクタリング後にフォールバックメッセージが消える、といったケースです。code では小さな失敗でも、本番環境では大きなコストになります。
React のテストは大きく変わりました。その転機は React Testing Library が、内部実装ではなく挙動をテストする主流モデルになったときですこれにより、チームはコンポーネントの props や state ではなく、ユーザーの挙動を模したテストへとシフトしました。これは React Native のテストガイドライン React Nativeのテスト概要にも反映されています。このシフトが重要なのは、React の code は常に再構成されるからです。Hooks が移動し、コンポーネントが分割され、Context が導入されます。内部構造に依存したテストは健全なリファクタリングでも壊れますが、目に見える挙動に紐付いたテストなら通常は生き残ります。
ユニットテストが守るべきもの
優れた React のユニットテストは、一つの小さな「契約」を守ります。
- レンダリング出力: ユーザーは正しいテキスト、ラベル、状態、フォールバックを確認できますか?
- インタラクションの挙動: クリック、入力、トグル操作でUIが正しく変化しますか?
- 境界処理: 期待される入力、データ欠落、エラーパスの場合に、コンポーネントは正しく動作しますか?
弱いテストは、間違ったものを保護します:
- コンポーネント内部: State の形状、プライベートメソッド、実装専用の props
- フレームワークの仕組み: React が内部で期待通りに hook を更新したかどうか
- 子コンポーネントの詳細: ここで検証する意図のない、ネストされたコンポーネントが所有するマークアップ
実践的なルール: ユーザーが見たり操作したりするものを変わらないようにコンポーネントをリファクタリングできるなら、テストも変える必要はありません。
ユニットテストはより広いテストシステムの一部です。アプリ全体がエンドツーエンドで動作することを証明しようとするものではありません。ブラウザレベルのテストやデバイスレベルの検証パスが必要になる前に、リグレッションを検出するための高速レイヤーです。それが、あらゆる合理的なスタックにおいて第一の防御線である理由です。 本番アプリのための自動テスト.
頻繁にリリースするReactチームにとって、この役割分担が信頼につながります。ユニットテストはローカルのリグレッションを素早く検出し、統合テストは結合部を検証し、E2Eテストはクリティカルパスを確認します。ユニット層をスキップすると、下流のより遅いテストに過度な負荷がかかります。
モダンなReactテスト環境の構築方法
脆弱なテスト環境は、アサーションを書く前にフラakyテストを生み出します。多くの開発者は、根本的な問題がローカルマシンとCI間の設定の不整合にあるにもかかわらず、Jest、jsdom、またはReactを責めます。解決策は、環境を「退屈」にすることです。ここでは退屈であることが重要です。

予測可能なランナーと環境から始める
モダンなReactアプリ、特にViteで作成されたものでは、ベースラインセットアップには以下を含めるべきです:
- テストランナー: Jestは依然として一般的で、特に古いReactコードベースやエンタープライズCIスタックでよく使われています。
- ブラウザに近い環境:
jsdomコンポーネントテストがDOM出力をレンダリングできるようにします。 - Testing Libraryのユーティリティ:
@testing-library/reactと@testing-library/jest-dom - セットアップ用の単一エントリポイント: matcherとグローバルmockを登録する1つのファイル
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"
}
}
短い手順書は、新しいチームメンバーが素早く同じベースラインに到達するのを助けます:
最も影響の大きいセットアップの選択は、デフォルト値に対する規律です。エイリアスは設定ファイルに、環境のモックは1つのセットアップファイルに集約します。 jsdom UIテストにはこれを使用し、純粋なユーティリティには可能な限り軽量な環境を使用します。個々のテストが必要とするカスタム動作が少なければ少ないほど、システムは信頼性が高くなります。
意味のあるコンポーネントテストを書く
テストを書くこと自体に課題があるわけではありません。課題は、6ヶ月後も意味のあるテストを書けるかどうかです。
React コンポーネントのユニットテストにおける標準的なパターンは、今も正解です: コンポーネントをレンダリングし、ユーザー視点のセレクタで UI をクエリし、インタラクションをトリガーして、結果としての DOM 変更をアサートする。これにより、state や props といった実装詳細からテストを切り離せます。 React テストガイド。ポイントは、このパターンを抑制的に適用することです。
ユーザーが使うようにアコーディオンをテストする
の基本的な Accordion コンポーネントを考えます。タイトル付きのボタンをレンダリングし、パネルの内容は非表示から始まります。ボタンをクリックすると内容が表示され、アクセシビリティ状態が更新されます。
これだけで、いくつかの有用なテストを書くのに十分な挙動があります:
- 初期レンダリングではタイトルは表示されるが、内容は表示されない。
- トリガーをクリックすると、コンテンツが表示されます。
- もう一度クリックすると、折りたたまれます。
- アクセシビリティ属性は、表示状態を反映します。
最後のポイントは、あまりにも頻繁にスキップされます。コンポーネントが aria-expanded, aria-controls、またはロールベースの構造を使用している場合、それらを検証してください。それらは実装の詳細ではなく、ユーザー向けの契約の一部です。
最高のコンポーネントテストは、決して受け取りたくないバグレポートのように読めます。
意図に基づいてクエリを選ぶ
React Testing Library は複数のクエリスタイルを提供しますが、それらは互換性がありません。間違ったものを選ぶと、テストがノイズだらけになり、誤解を招くことになります。
| クエリタイプ | 要素が見つかった場合 | 要素が見つからない場合 | ユースケースの例 |
|---|---|---|---|
getBy |
要素を即座に返す | 即座にエラーをスローする | ボタンや見出しが既に画面上にあることを検証 |
queryBy |
要素を即座に返す | 戻り値 null |
操作前に非表示コンテンツが存在しないことを検証 |
findBy |
要素が出現すると解決される | 待機後に拒否される | fetch や遅延更新の後に、非同期で読み込まれたコンテンツが表示されることをアサートする |
シンプルなメンタルモデルが役立ちます:
- 使用する
getByすでに存在しているべきものに対して。 - 使用する
queryByまだ存在してはいけないものに対して。 - 使用する
findBy後でUIが変わったとき。
テストが findBy を何にでも使う形で始まる場合、作者はコンポーネントがいつ更新されるか確信を持っていないことが多いです。その不確実性が後でテストが不安定になる原因になります。
アコーディオンの実践的な例
代表的なコンポーネントの例です:
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ベースのクエリを優先する: ボタン、見出し、ダイアログ、アラート、inputは通常roleで取得すべきです。
- 各テストの範囲を絞る: 1テストにつきユーザーに見える振る舞いを1つにすると、失敗が読みやすくなります。
- テストは結果に基づいて命名する: 「正しく動作する」よりも「開いたときに aria-expanded が更新される」の方が、はるかに有用です。
DOM経由でテストしにくいコンポーネントは、設計上の問題を示していることがよくあります。状態を間違った場所に隠しているのかもしれません。セマンティックなマークアップが欠けているのかもしれません。優れたテストは、チームをより良いコンポーネントへと後押しします。
カスタムHookとアプリケーションロジックのテスト
React アプリでは、コンポーネントの外側に多くの重要な動作が隠れています。状態遷移はフックに、バリデーションやフォーマットはヘルパー関数に、データ整形はレンダリング前に発生することが多いです。表示されるコンポーネントのみをテストすると、本番環境の動作を壊しうる code の大部分を見逃してしまいます。
HooksにはReact対応のハーネスが必要
カスタムフックは正しく実行するために React が必要なので、以下でテストします renderHook でラップし、状態を変更する呼び出しを以下で囲みます act().
小さな useToggle フックが良い例です:
import { useState, useCallback } from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(current => !current), []);
return { value, toggle };
}
そのテストは、パブリックな契約に焦点を当てるべきです:
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);
});
フック自体がユニットであるため、このテストは有用です。React の内部を実装しているわけではありません。フックの外部動作を検証しているのです。
再利用可能な UI や機能プリミティブを構築するプロダクトチームにとって、このパターンは非常に重要です。フックは、アプリ、デザインシステム、社内ツール間で共有されるインターフェースとして機能することがよくあります。商用意図を持って再利用可能な動作を設計している場合、 メーカー向けプロダクトのフック に関するリソースが、フックを実装詳細ではなく、プロダクト化されたビルディングブロックとして位置づけるのに役立ちます。
純粋なロジックはテストでも純粋に保つ
すべてに必要なのは 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()、必要に応じてラッパープロバイダー。 - ユーティリティ: 素のJestを使い、DOMは使いません。
- 状態を持つ横断的なロジック: コンポーネントのテストが肥大化してきたら、テスト可能なヘルパーに切り出しましょう。
コンポーネントテストに、本来はより下位レイヤーで検証すべきロジックのアサーションを詰め込みすぎるチームは少なくありません。そのロジックを切り出すと2つのメリットがあります。コンポーネントテストはクリーンになり、ロジックテストは速くなります。
高度なテクニック:MockとAsyncをマスターする
信頼性の低いReactテストスイートの多くは、2つの箇所で壊れます。依存関係の境界と、時間まわりです。
非同期テストとモックは、おもちゃのテストスイートとリリース前に信頼できるテストスイートを分ける境界線です。ある分析では、 テストのflakinessの46.5%を、非同期のタイミングといった環境やリソース関連の問題に起因するとしています。 における このReactユニットテスト分析。Reactアプリでは、これは状態遷移、レンダリングの遅延、ネットワーク駆動のUI、そして決定論的に待たずに推測するテストに直結します。

全レイヤーではなく境界をMockする
誤解を招くテストを書く一番手っ取り早い方法は、コンポーネントツリーの半分をモックし、自分のモックが動いたとアサートすることです。
アカウントデータを取得するコンポーネントでは、ネットワーククライアントまたは API モジュールをモックします。テストでその境界の分離が本当に必要でない限り、hook、子の行コンポーネント、ローディングスピナー、3つのユーティリティ関数はモックしないでください。
以下のルールセットに従ってください:
- 外部サービスをモックする: HTTPクライアント、アナリティクス、ブラウザ専用API、ネイティブブリッジ
- 不安定なプラットフォームAPIをモックする:
matchMediaタイマー、Electronのpreloadインターフェース、jsdomで利用できない Capacitor プラグイン - デフォルトでは自作の内部実装をモックしない: カスタムhook、単純な子コンポーネント、ローカルユーティリティ
難しい部分をすべてフェイクに置き換えてテストが通っても、リリースへの自信にはほとんどつながりません。
ランナーAPIに関する例やパターンを求めているチームにとって、Capgo テストチュートリアル は実用的なリファレンスライブラリとなります。特に、Reactは知っているがテストの仕組みにはまだ慣れていない開発者のオンボーディングに最適です。
タイミングが曖昧だとAsyncテストは失敗する
非同期の失敗は、主に以下の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 1つの要素の出現が関心のあるイベントである場合。 waitFor 条件が広範囲である場合、または単一のクエリで状態を表現できない場合。 setTimeout テスト内で、タイマーの動作を明示的にテストし、フェイクタイマーを使用している場合を除き、使用を避けてください。
Reactのテストエコシステムも、これを尊重することを期待しています。 act() 更新周りのセマンティクスです。Testing Library が多くの部分を処理してくれますが、手動で状態を操作したりタイマーを進めたりする場合は、更新がいつ flush されるかを考える必要があります。
適切なMockツールを選択する
モックツールごとに解決する課題は異なります:
| ツール | 最適な用途 | よくある間違い |
|---|---|---|
jest.fn() |
独立したフェイクコールバックや注入された関数 | シンプルなコールバックで十分なのに、モジュール全体を置き換えてしまうこと |
jest.spyOn() |
実際のオブジェクトやモジュールの1つのメソッドを監視または上書きする | 元の実装を復元し忘れること |
jest.mock() |
import 境界でモジュール依存を置き換える | デフォルトで巨大なモジュールをモック化し、意味のある挙動を失ってしまうこと |
例の助けになります:
- 使うのは
jest.fn()コンポーネントがonSubmitprop を受け取る場合 - 使用する
jest.spyOn()検証が必要な場合console.error、ストレージメソッド、またはエクスポートされた API 呼び出し。 - 使用する
jest.mock()モジュールのインポートが I/O、ネイティブ code、またはユニット境界外の動作に到達する場合。
多くのガイドが十分に扱っていない高度な領域の一つが、モダン React におけるエラーパスのテストです。エラーバウンダリ、遅延状態変更、非同期フォールバック UI は、単なる「ハッピーパス」のクリック例ではなく、第一級のテストに値します。子コンポーネントが例外をスローしたら、フォールバック UI をアサートします。リクエストが失敗したら、表示されるリカバリー状態をアサートします。ローディング中にボタンが無効化される場合も、それをアサートします。ユーザーが記憶に残るバグは、まさにこれらです。
テストの品質と戦略を向上させる
多くのチームは、カバレッジを信頼性と同じものとして追い続けています。そうではありません。
カバレッジ目標を達成しても、重要なリグレッションを見逃すことはあります。浅いアサーション、広すぎるスナップショット、内部実装のモックで埋め尽くされたテストスイートは、安全であるかのような錯覚を与えながら、メンテナンスコストも増やします。

カバレッジは地図であって、ゴールではない
カバレッジレポートは、「まだ保護されていないクリティカルパスはどこか」という1つの問いに答えるときに役立ちます。
単にパーセンテージを上げるためだけに、些細なラッパーや静的なマークアップ、1行だけのパススルーファイルをテストするよう開発者に強いるなら、役に立ちません。カバレッジは発見ツールとして扱いましょう。認証状態、課金処理、フィーチャーフラグ、アップデートプロンプトにテストがなければ、それはシグナルです。一方、表示専用のアイコンコンポーネントにテストがないのは、通常はシグナルではありません。
健全なレビューでの問いはシンプルです。「このテストはリリースリスクを減らすか?」
- はい: クリティカルパス上のユーザーに見える振る舞いを検証している。
- たぶん: リファクタリング時に壊れやすいビジネスロジックを保護している。
- いいえ: 実装の詳細をアサートしている、あるいは他のテストと価値が重複している。
ユニットテストすべきでないもの
多くのReactガイドは、テストの「除外」に十分な時間を割いていません。このギャップは重要です。過剰なモックや実装詳細のテストは、ユーザー体験が壊れているのにテストが通るような脆いスイートを生み出します。BrowserStackのガイドでも指摘されているように、 Reactでユニットテストすべきでないもの.
これらのパターンはスキップするか、大幅に制限してください:
- 内部状態のアサーション: テストしないこと
isOpenパネルが開いたかをテストできる場合、直接テストしないこと。 - フレームワークの動作: React がエフェクトを呼び出したことをテストしない。エフェクトが変更した結果をテストすること。
- サードパーティ製ライブラリの内部: 日付ピッカーやルーターとの統合をテストし、ライブラリ自体のレンダリングロジックはテストしないこと。
- 細分化しすぎたユニットテスト: 子コンポーネントやヘルパーをすべてモックすると、意味のある振る舞いをテストできていないかもしれません。
悪いテストは、テストがない状態より厄介です。リファクタリングを妨げるうえ、本番のバグも検知できないからです。
役立つヒューリスティックは「境界の所有権」です。code が所有する範囲をテストしてください。React、ブラウザ、成熟したライブラリがすでに担っている部分は、統合レイヤーが契約を変えない限りテストしないでください。
Snapshotのメリットとデメリット
スナップショットが無意味なわけではありません。ただ、誤用しやすいだけです。
出力が安定していてシンプル、かつ構造的な差分が意味を持つコンポーネントに限定して、控えめに使いましょう。インタラクティブなコンポーネントや動きの多いコンポーネントでは、ノイズになるので避けてください。開発者は中身を読まなくなり、反射的に更新するようになります。
より良い代替手段がたいていあります:
- 条件付きレンダリングでは、キーとなるテキストの有無を検証します。
- 視覚的な状態変化では、重要な role、label、属性を検証します。
- エラーやフォールバックでは、実際のメッセージやアラート領域を検証します。
ユニットテストを超えて、より広範な品質プロセスが必要なら、心強い味方になるのが アプリ品質保証ワークフロー テスト、リリースチェック、ロールバック計画を一つのシステムとして扱うこと。これがテスト品質を最も速く向上させるマインドセットの転換点です。「テストを何本持っているか」ではなく、「どの失敗がユーザーに届く可能性があるか」を問い始めましょう。
クロスプラットフォームCI/CDパイプラインへのテスト統合
開発者のラップトップだけで実行されるテストスイートは、管理ではなく提案に過ぎません。
すべてのプルリクエストがクリーンな環境で同じチェックを実行し、チェックが失敗した際にマージをブロックするようになった時点で、スイートは運用可能になります。これは自明のことのように聞こえますが、多くのチームには依然として重大なギャップがあります。テストは手動で実行され、カバレッジレポートは任意です。パッケージングやリリースジョブが、テストジョブの完了前に開始されます。こうした状況が、小さなUIの退行を大きなリリース失敗へと変えてしまいます。

プルリクエストは常に同じゲートを通過すべきである
Reactのユニットテストをセーフティネットとして機能させるには、CIにいくつかの必須要素が必要です:
- すべてのプルリクエストで実行
- ロックファイルから依存関係をインストール
- 毎回同じテストコマンドを使用
- テスト失敗時に素早く失敗させる
- テストが合格した後にのみ成果物を公開
これこそが、 アプリチームにおける継続的デプロイの核心です。リリース後にではなく、リリース前に自信をつけましょう。
多くのチームには、シンプルな 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メッセージング、デスクトップ専用の状態に依存することがありますが、これらは意図的にモックしない限り通常のブラウザテストには存在しません。
- 共有リリーストレイン: デプロイプロセスが公開を厳密にゲートしない場合、1つの不良bundleが複数のターゲットに影響を与える可能性があります。
そのため、unit testはpackagingジョブの前に、packagingジョブはdistributionジョブの前に実行されるべきです。各ステージでリスクを絞り込みます。unit testはローカルでのリグレッションを素早く検出し、プラットフォームpackagingは環境の前提条件を検証します。最終的なリリースの信頼性は、手動承認または段階的ロールアウトで確保します。
実践的な GitHub Actions ワークフロー
より成熟したパイプラインでは、通常、責任を以下のように分割します:
- テストジョブ: 高速なユニットテストとフックテスト
- ビルドジョブ: テストが合格した後にのみ本番ビルドを実行
- パッケージングジョブ: Capacitor の同期、Electron のパッケージング、または成果物のバンドリング
- リリースジョブ: 承認済みのブランチまたはタグからのみ公開
Capacitor または Electron アプリにライブアップデートを配信するチームにとって、ここがリリースツールが重要になる場面です。そのワークフローの一つの選択肢は Capgoです。これは、ロールバック機能とチャネルベースのロールアウト制御を備えた、CapacitorJS および Electron アプリ向けの署名済み Web バンドルを公開します。実際には、React のテストジョブが、Web バンドルがステージングや本番環境への配信に昇格する前の最初のハードゲートとして機能することを意味します。
運用ルールはシンプルです。テストが弱いことをリリースインフラで補わないでください。信頼できるテストで不良な変更を除外した後に、リリースインフラを使用してください。
信頼できるテストシステムはチームの行動を変えます。エンジニアは迷いなくマージし、レビュアーは基本の再実行ではなくエッジケースに集中できます。リリースマネージャーも、すべてのデプロイを賭け事のように扱わなくなります。これが、Reactのユニットテストを適切に行うことによる成果です。
チームがCapacitorまたはElectronを通じてReactをリリースする場合、リリースの安全性はローカルでのテスト成功だけでは担保されません。 Capgo 署名付きWebアップデートの公開、ロールアウトチャネルの指定、ストアレビューを待たずに不良なバンドルをロールバックできる管理された方法を提供します。これは、デプロイ前にユニットテストの合格を必須とするCIパイプラインの背後に自然に組み込むことができます。