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

テスト

コードベースが拡大するにつれて、予期しない小さなエラーやエッジケースが、より大きな障害へと連鎖する可能性があります。バグはユーザーエクスペリエンスを悪化させ、最終的にはビジネス損失につながります。脆弱なプログラミングを防ぐ1つの方法は、コードをリリースする前にテストすることです。

このガイドでは、静的解析からエンドツーエンドテストまで、アプリが期待どおりに機能することを確認するためのさまざまな自動化された方法について説明します。

Testing is a cycle of fixing, testing, and either passing to release or failing back into testing.

テストの必要性

私たちは人間であり、人間は間違いを犯します。テストは、これらの間違いを発見し、コードが機能していることを検証するのに役立つため重要です。おそらくさらに重要なのは、テストは、新しい機能を追加したり、既存の機能をリファクタリングしたり、プロジェクトの主要な依存関係をアップグレードしたりする際に、将来にわたってコードが機能し続けることを保証することです。

テストには、あなたが気づいている以上に価値があります。コードのバグを修正する最良の方法の1つは、バグを露呈する失敗するテストを作成することです。その後、バグを修正してテストを再実行し、合格すれば、バグが修正され、コードベースに再導入されることはありません。

テストは、チームに新しく加わるメンバーのドキュメントとしても機能します。コードベースを初めて見る人にとって、テストを読むことは既存のコードがどのように機能するかを理解するのに役立ちます。

最後に、より自動化されたテストは、手動でのQAにかかる時間を減らし、貴重な時間を解放します。

静的解析

コード品質を向上させるための最初のステップは、静的解析ツールの使用を開始することです。静的解析は、コードを実行せずに、コードを記述する際にエラーをチェックします。

  • リンターは、コードを解析して、未使用のコードなどの一般的なエラーを検出し、落とし穴を回避するのに役立ちます。スペースの代わりにタブを使用する(またはその逆、設定による)などのスタイルガイド違反にフラグを立てます。
  • 型チェックは、関数に渡す構成要素が、その関数が受け入れるように設計されたものと一致することを確認し、たとえば、数値を期待するカウント関数に文字列を渡すことを防ぎます。

React Nativeには、リンティング用のESLintと型チェック用のTypeScriptという2つのツールがすぐに使えるように設定されています。

テスト可能なコードの記述

テストを開始するには、まずテスト可能なコードを記述する必要があります。航空機製造プロセスを考えてみましょう。複雑なシステムすべてがうまく連携することを示すために、モデルが最初に離陸する前に、個々の部品が安全かつ正しく機能することを保証するためにテストされます。たとえば、翼は極端な負荷の下で曲げられてテストされます。エンジン部品は耐久性についてテストされます。フロントガラスは、シミュレートされた鳥の衝突に対してテストされます。

ソフトウェアも同様です。プログラム全体を多数のコード行を持つ1つの巨大なファイルに記述する代わりに、アセンブルされた全体をテストするよりも徹底的にテストできる複数の小さなモジュールにコードを記述します。このようにして、テスト可能なコードを記述することは、クリーンでモジュール化されたコードを記述することと密接に絡み合っています。

アプリをよりテスト可能にするには、まずアプリのビュー部分 (React コンポーネント) をビジネスロジックとアプリの状態 (Redux、MobX、その他のソリューションを使用しているかどうかに関わらず) から分離することから始めます。これにより、React コンポーネントに依存すべきではないビジネスロジックのテストを、主にアプリの UI をレンダリングする役割を持つコンポーネント自体から独立させることができます!

理論的には、すべてのロジックとデータ取得をコンポーネントから移動させることまでできます。この方法では、コンポーネントはレンダリングだけに専念します。状態はコンポーネントから完全に独立します。アプリのロジックは、React コンポーネントがなくてもまったく機能します!

ヒント

テスト可能なコードに関するトピックを、他の学習リソースでさらに探求することをお勧めします。

テストの記述

テスト可能なコードを記述したら、いよいよ実際のテストを記述します!React Nativeのデフォルトテンプレートには、Jestテストフレームワークが同梱されています。この環境に合わせて調整されたプリセットが含まれているため、設定やモックをすぐに調整することなく生産的に作業できます—まもなくモックの詳細を説明します。Jestを使用して、このガイドで紹介されているすべての種類のテストを記述できます。

テスト駆動開発を行う場合、実際には最初にテストを書きます!そうすれば、コードのテスト可能性が確保されます。

テストの構造化

テストは短く、理想的には1つのことだけをテストするべきです。Jestで記述されたユニットテストの例から始めましょう

js
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});

テストはit関数に渡される文字列によって記述されます。何がテストされているかを明確にするために、説明を注意深く記述してください。以下の点をカバーするように最善を尽くしてください

  1. Given - ある前提条件
  2. When - テストしている関数によって実行されるあるアクション
  3. Then - 期待される結果

これはAAA(Arrange、Act、Assert)としても知られています。

Jestは、テストを構造化するのに役立つdescribe関数を提供します。describeを使用して、1つの機能に属するすべてのテストをグループ化します。必要に応じて、describeはネストできます。一般的に使用するその他の関数は、テストしているオブジェクトを設定するために使用できるbeforeEachまたはbeforeAllです。Jest APIリファレンスで詳細をお読みください。

テストに多くのステップや多くの期待が含まれている場合は、おそらくそれを複数の小さなテストに分割したいと思うでしょう。また、テストが互いに完全に独立していることを確認してください。スイート内の各テストは、他のテストを最初に実行することなく、それ自体で実行可能でなければなりません。逆に、すべてのテストを一緒に実行しても、最初のテストが2番目のテストの出力に影響を与えてはなりません。

最後に、開発者として、コードがうまく機能し、クラッシュしないことを好みます。テストでは、これはしばしば逆です。失敗したテストを良いものと考えてください!テストが失敗するとき、それはしばしば何かが正しくないことを意味します。これにより、ユーザーに影響を与える前に問題を修正する機会が得られます。

ユニットテスト

ユニットテストは、個々の関数やクラスなど、コードの最小部分をカバーします。

テスト対象のオブジェクトに依存関係がある場合、次の段落で説明するように、それらをモックアウトする必要があることがよくあります。

ユニットテストの素晴らしい点は、記述と実行が迅速であることです。そのため、作業中にテストが合格しているかどうかについて迅速なフィードバックが得られます。Jestには、編集中のコードに関連するテストを継続的に実行するオプションさえあります。ウォッチモード

モック

テスト対象のオブジェクトが外部依存関係を持っている場合、「モックアウト」したいことがあります。「モック」とは、コードの依存関係を独自の実装に置き換えることです。

情報

一般的に、テストで実際のオブジェクトを使用する方が、モックを使用するよりも良いですが、それが不可能な状況もあります。たとえば、JSユニットテストがJavaまたはObjective-Cで書かれたネイティブモジュールに依存している場合です。

あなたの街の現在の天気を示すアプリを書いていて、天気情報を提供する外部サービスまたは他の依存関係を使用していると想像してください。サービスが雨が降っていると告げたら、雨雲の画像を表示したいとします。テストでそのサービスを呼び出したくないのは、以下の理由によります。

  • (ネットワークリクエストが関与するため)テストが遅く不安定になる可能性があります
  • テストを実行するたびにサービスが異なるデータを返す可能性があります
  • サードパーティサービスは、本当にテストを実行する必要があるときにオフラインになる可能性があります!

したがって、サービスのモック実装を提供することで、数千行のコードとインターネットに接続されたいくつかの温度計を効果的に置き換えることができます!

Jestは、関数レベルからモジュールレベルのモックまで、モックをサポートしています。

統合テスト

大規模なソフトウェアシステムを記述する場合、個々の部分が互いに相互作用する必要があります。ユニットテストでは、ユニットが別のユニットに依存している場合、その依存関係をモックアウトし、偽のものに置き換えることがあります。

統合テストでは、実際の個々のユニットが(アプリ内と同じように)結合され、それらの連携が期待どおりに機能することを確認するために一緒にテストされます。これは、ここでモックが行われないという意味ではありません。まだモックが必要になるでしょう(たとえば、天気サービスとの通信をモックするためなど)が、ユニットテストよりもはるかに少ない頻度で必要になります。

情報

統合テストが何を意味するかという用語は常に一貫しているわけではないことに注意してください。また、ユニットテストと統合テストの境界線が常に明確であるとは限りません。このガイドでは、テストが次の場合、「統合テスト」に分類されます

  • 上記のように、アプリのいくつかのモジュールを結合します
  • 外部システムを使用します
  • 他のアプリケーション(天気予報サービスAPIなど)に対してネットワーク呼び出しを行います
  • あらゆる種類のファイルまたはデータベースI/Oを実行します

コンポーネントテスト

Reactコンポーネントはアプリのレンダリングを担当し、ユーザーはその出力と直接対話します。アプリのビジネスロジックが高いテストカバレッジを持ち、正しくても、コンポーネントテストがなければ、壊れたUIをユーザーに提供してしまう可能性があります。コンポーネントテストはユニットテストと統合テストの両方に分類される可能性がありますが、React Nativeの中核部分であるため、別途説明します。

Reactコンポーネントをテストする場合、次の2つのことをテストしたい場合があります

  • インタラクション: ユーザーによって操作されたときにコンポーネントが正しく動作することを確認する (例: ユーザーがボタンを押したとき)
  • レンダリング: React が使用するコンポーネントのレンダリング出力が正しいことを確認する (例: UI におけるボタンの見た目と配置)

たとえば、onPressリスナーを持つボタンがある場合、ボタンが正しく表示され、ボタンをタップするとコンポーネントによって正しく処理されることをテストしたいとします。

これらのテストに役立つライブラリがいくつかあります。

  • React Native Testing Libraryは、Reactのテストレンダラーを基盤として構築されており、次の段落で説明するfireEventqueryAPIを追加しています。
  • [非推奨] Reactのコアと並行して開発されたTest Rendererは、DOMやネイティブモバイル環境に依存することなく、Reactコンポーネントを純粋なJavaScriptオブジェクトにレンダリングするために使用できるReactレンダラーを提供します。
警告

コンポーネントテストは、Node.js環境で実行されるJavaScriptテストのみです。これらは、React Nativeコンポーネントを支えるiOS、Android、またはその他のプラットフォームコードを考慮に入れていません。したがって、すべてがユーザーにとって機能することについて100%の信頼を与えることはできません。iOSまたはAndroidコードにバグがある場合、それらを見つけることはできません。

ユーザーインタラクションのテスト

UIのレンダリング以外に、コンポーネントはTextInputonChangeTextButtononPressのようなイベントを処理します。また、他の関数やイベントコールバックを含むこともあります。次の例を考えてみましょう

tsx
function GroceryShoppingList() {
const [groceryItem, setGroceryItem] = useState('');
const [items, setItems] = useState<string[]>([]);

const addNewItemToShoppingList = useCallback(() => {
setItems([groceryItem, ...items]);
setGroceryItem('');
}, [groceryItem, items]);

return (
<>
<TextInput
value={groceryItem}
placeholder="Enter grocery item"
onChangeText={text => setGroceryItem(text)}
/>
<Button
title="Add the item to list"
onPress={addNewItemToShoppingList}
/>
{items.map(item => (
<Text key={item}>{item}</Text>
))}
</>
);
}

ユーザーインタラクションをテストするときは、ユーザーの視点からコンポーネントをテストします。ページには何がありますか?操作すると何が変わりますか?

経験則として、ユーザーが見たり聞いたりできるものを使用することを推奨します。

逆に、避けるべきこと

  • コンポーネントのプロパティや状態に対してアサーションを行うこと
  • testIDクエリ

プロパティや状態のような実装詳細のテストは避けてください。そのようなテストは機能しますが、ユーザーがコンポーネントとどのように対話するかに焦点を当てておらず、リファクタリングによって壊れやすい傾向があります(たとえば、いくつかの名前を変更したり、フックを使用してクラスコンポーネントを書き直したりする場合)。

情報

Reactクラスコンポーネントは、内部状態、プロパティ、イベントハンドラーなどの実装詳細をテストする傾向があります。実装詳細のテストを避けるには、フックを使用する関数コンポーネントを使用することを推奨します。これにより、コンポーネントの内部に依存することがより難しくなります。

React Native Testing Libraryなどのコンポーネントテストライブラリは、提供されるAPIを慎重に選択することで、ユーザー中心のテスト記述を容易にします。次の例では、ユーザーがコンポーネントと対話するのをシミュレートするfireEventメソッドのchangeTextpress、およびレンダリングされた出力で一致するTextノードを見つけるクエリ関数getAllByTextを使用しています。

tsx
test('given empty GroceryShoppingList, user can add an item to it', () => {
const {getByPlaceholderText, getByText, getAllByText} = render(
<GroceryShoppingList />,
);

fireEvent.changeText(
getByPlaceholderText('Enter grocery item'),
'banana',
);
fireEvent.press(getByText('Add the item to list'));

const bananaElements = getAllByText('banana');
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});

この例では、関数を呼び出したときに状態がどのように変化するかはテストしていません。ユーザーがTextInputでテキストを変更し、Buttonを押したときに何が起こるかをテストしています!

レンダリング出力のテスト

スナップショットテストは、Jestによって可能になる高度なテストの一種です。これは非常に強力で低レベルなツールなので、使用する際には細心の注意が必要です。

「コンポーネントスナップショット」とは、Jestに組み込まれたカスタムReactシリアライザーによって作成されるJSXライクな文字列のことです。このシリアライザーにより、JestはReactコンポーネントツリーを人間が読みやすい文字列に変換できます。言い換えれば、コンポーネントスナップショットとは、テスト実行中に生成されるコンポーネントのレンダリング出力のテキスト表現です。それは次のように見えるかもしれません

tsx
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>

スナップショットテストでは、通常、まずコンポーネントを実装し、次にスナップショットテストを実行します。するとスナップショットテストはスナップショットを作成し、それをリポジトリ内のファイルとして参照スナップショットとして保存します。このファイルはコミットされ、コードレビュー中にチェックされます。コンポーネントのレンダリング出力に対する今後の変更は、そのスナップショットを変更し、テストを失敗させます。その後、テストが合格するように保存された参照スナップショットを更新する必要があります。その変更は再びコミットされ、レビューされる必要があります。

スナップショットにはいくつかの弱点があります

  • 開発者やレビュー担当者にとって、スナップショットの変更が意図されたものなのか、バグの証拠なのかを判断するのは難しい場合があります。特に大きなスナップショットは、すぐに理解しにくくなり、その付加価値は低くなります。
  • スナップショットが作成された時点で、レンダリングされた出力が実際には間違っている場合でも、正しいと見なされます。
  • スナップショットが失敗した場合、変更が予期されるものかどうかを適切に調査せずに、--updateSnapshot Jestオプションを使用してスナップショットを更新したくなるものです。したがって、ある程度の開発規律が必要です。

スナップショット自体は、コンポーネントのレンダリングロジックが正しいことを保証するものではなく、単に予期しない変更に対する保護や、テスト対象のReactツリー内のコンポーネントが期待されるプロパティ(スタイルなど)を受け取っていることを確認するのに優れています。

小さいスナップショットのみを使用することを推奨します(no-large-snapshotsルールを参照)。2つのReactコンポーネントの状態間の変更をテストしたい場合は、snapshot-diffを使用してください。迷った場合は、前の段落で説明した明示的な期待を優先してください。

エンドツーエンドテスト

エンドツーエンド(E2E)テストでは、デバイス(またはシミュレーター/エミュレーター)上で、ユーザーの視点からアプリが期待どおりに動作していることを検証します。

これは、リリース構成でアプリをビルドし、それに対してテストを実行することによって行われます。E2Eテストでは、Reactコンポーネント、React Native API、Reduxストア、またはビジネスロジックについて考える必要はなくなります。それがE2Eテストの目的ではなく、E2Eテスト中にそれらにアクセスすることもできません。

代わりに、E2Eテストライブラリを使用すると、アプリの画面上の要素を見つけて制御できます。たとえば、実際のユーザーと同じようにボタンを実際にタップしたり、TextInputsにテキストを挿入したりできます。次に、特定の要素がアプリの画面に存在するかどうか、表示されているかどうか、どのようなテキストが含まれているかなどについてアサーションを行うことができます。

E2Eテストは、アプリの一部が機能していることについて最高の信頼を与えてくれます。トレードオフには、次のようなものがあります。

  • 他の種類のテストと比較して、作成に時間がかかります
  • 実行が遅い
  • 不安定になりやすい(「不安定な」テストとは、コードに何の変更もなくてもランダムに合格したり失敗したりするテストのこと)

E2Eテストでアプリの重要な部分(認証フロー、コア機能、支払いなど)をカバーするようにしてください。アプリの重要でない部分には、より高速なJSテストを使用してください。テストを追加すればするほど信頼性は高まりますが、その分、維持と実行に費やす時間も増えます。トレードオフを考慮し、自分にとって最適なものを決定してください。

いくつかのE2Eテストツールが利用可能です。React Nativeコミュニティでは、DetoxがReact Nativeアプリに特化しているため人気のフレームワークです。iOSおよびAndroidアプリの分野で人気のある他のライブラリには、AppiumMaestroがあります。

まとめ

このガイドを読んで楽しんで、何かを学んでいただけたなら幸いです。アプリをテストする方法はたくさんあります。最初はどれを使用すべきか決めるのが難しいかもしれません。しかし、素晴らしいReact Nativeアプリにテストを追加し始めれば、すべてが理解できると信じています。さあ、何を待っていますか?カバレッジを向上させましょう!


このガイドは、Vojtech Novakによって完全に執筆・寄稿されました。