テスト
コードベースが拡大するにつれて、予期しない小さなエラーやエッジケースが大きな障害に発展する可能性があります。バグは悪いユーザーエクスペリエンスにつながり、最終的にはビジネスの損失につながります。脆弱なプログラミングを防ぐ1つの方法は、コードをリリースする前にテストすることです。
このガイドでは、静的解析からエンドツーエンドテストまで、アプリが期待どおりに動作することを保証するためのさまざまな自動化された方法について説明します。
なぜテストをするのか
私たちは人間であり、人間は間違いを犯します。テストは、これらの間違いを発見し、コードが機能していることを確認するのに役立つため、重要です。おそらくさらに重要なことは、新しい機能を追加したり、既存の機能をリファクタリングしたり、プロジェクトの主要な依存関係をアップグレードしたりする場合に、コードが将来も引き続き機能することをテストによって保証することです。
テストには、あなたが思っている以上の価値があります。コードのバグを修正する最良の方法の1つは、バグを明らかにする失敗するテストを書くことです。その後、バグを修正してテストを再実行すると、テストに合格した場合、バグが修正され、コードベースに再導入されないことを意味します。
テストは、チームに参加する新しい人のためのドキュメントとしても役立ちます。コードベースを初めて見る人にとって、テストを読むことは、既存のコードがどのように機能するかを理解するのに役立ちます。
最後に、自動化されたテストが多ければ多いほど、手動のQAに費やす時間が短縮され、貴重な時間を節約できます。
静的解析
コードの品質を向上させるための最初のステップは、静的解析ツールを使用することです。静的解析は、コードを書いているときにエラーをチェックしますが、そのコードを実行することはありません。
- リンターは、コードを分析して、未使用のコードなどの一般的なエラーをキャッチし、ピットフォールを回避し、スペースの代わりにタブを使用する(または構成に応じてその逆)など、スタイルガイドに反するものをフラグ付けします。
- 型チェックは、関数に渡しているコンストラクトが、関数が受け入れるように設計されたものと一致することを保証し、たとえば、数値を期待するカウント関数に文字列を渡すことを防ぎます。
React Nativeには、すぐに使用できる2つのツールが付属しています。ESLintはリンティング用、TypeScriptは型チェック用です。
テスト可能なコードを書く
テストを開始するには、まずテスト可能なコードを作成する必要があります。航空機の製造プロセスを考えてみましょう。どのモデルも最初に離陸して複雑なシステムがすべてうまく連携していることを示す前に、個々の部品がテストされて、安全で正しく機能することが保証されます。たとえば、翼は極端な荷重の下で曲げることによってテストされます。エンジン部品は耐久性についてテストされます。フロントガラスは、シミュレートされた鳥の衝突に対してテストされます。
ソフトウェアも同様です。プログラム全体を何行ものコードを含む1つの巨大なファイルに書く代わりに、組み立てられた全体をテストする場合よりも徹底的にテストできる複数の小さなモジュールにコードを書きます。このように、テスト可能なコードを書くことは、クリーンでモジュール化されたコードを書くことと絡み合っています。
アプリをよりテストしやすくするには、アプリのビュー部分(Reactコンポーネント)をビジネスロジックとアプリの状態から分離することから始めます(Redux、MobX、またはその他のソリューションを使用するかどうかは関係ありません)。こうすることで、ビジネスロジックのテスト(Reactコンポーネントに依存しないはずです)を、アプリのUIのレンダリングが主な役割であるコンポーネント自体から独立させることができます。
理論的には、すべてのロジックとデータフェッチをコンポーネントから移動することもできます。こうすることで、コンポーネントはレンダリング専用になります。状態はコンポーネントとは完全に独立します。アプリのロジックは、Reactコンポーネントがなくても機能します。
他の学習リソースでテスト可能なコードのトピックをさらに調べてみることをお勧めします。
テストを書く
テスト可能なコードを作成したら、いよいよ実際のテストを作成します。React Nativeのデフォルトテンプレートには、Jestテストフレームワークが付属しています。この環境に合わせて調整されたプリセットが含まれているため、構成を調整することなくすぐに生産性を高めることができます。モックについては後述します。Jestを使用して、このガイドで紹介されているすべてのタイプのテストを作成できます。
テスト駆動開発を行う場合は、実際には最初にテストを作成します。そうすれば、コードのテスト容易性が確保されます。
テストの構成
テストは短く、理想的には1つのことだけをテストする必要があります。Jestで書かれた単体テストの例から始めましょう
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});
テストは、it
関数に渡される文字列によって記述されます。何がテストされているかが明確になるように、説明を書く際には十分に注意してください。次の点を網羅するようにしてください
- 前提条件 - 何らかの前提条件
- 実行 - テスト対象の関数によって実行される何らかのアクション
- 結果 - 期待される結果
これは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のコアと共に開発されたReactのテストレンダラーは、DOMやネイティブモバイル環境に依存せずに、Reactコンポーネントを純粋なJavaScriptオブジェクトにレンダリングするために使用できるReactレンダラーを提供します。
- React Native Testing Libraryは、Reactのテストレンダラーをベースに構築されており、次の段落で説明する
fireEvent
およびquery
APIを追加します。
コンポーネントテストは、Node.js環境で実行されるJavaScriptテストのみです。React Nativeコンポーネントを支えているiOS、Android、またはその他のプラットフォームコードは考慮*されません*。したがって、ユーザーにとってすべてが機能するという100%の確信を与えることはできません。iOSまたはAndroidコードにバグがある場合、それらは見つかりません。
ユーザーインタラクションのテスト
UIのレンダリングに加えて、コンポーネントはTextInput
のonChangeText
やButton
のonPress
などのイベントを処理します。また、他の関数やイベントコールバックが含まれている場合もあります。次の例を考えてみましょう。
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クエリ
プロップや状態などの実装 details のテストは避けてください。このようなテストは機能しますが、ユーザーがコンポーネントとどのように対話するかを重視しておらず、リファクタリングによって破損する傾向があります(たとえば、名前を変更したり、フックを使用してクラスコンポーネントを書き直したりする場合)。
Reactクラスコンポーネントは、内部状態、プロップ、イベントハンドラーなど、実装 details をテストする傾向が特に強いです。実装 details のテストを避けるために、コンポーネントの内部に依存することを*難しく*するHooksを使用した関数コンポーネントを使用することをお勧めします。
React Native Testing Libraryなどのコンポーネントテストライブラリは、提供されるAPIを慎重に選択することにより、ユーザー中心のテストの作成を容易にします。次の例では、ユーザーがコンポーネントを操作することをシミュレートするfireEvent
メソッドのchangeText
とpress
、およびレンダリングされた出力で一致するText
ノードを見つけるクエリ関数getAllByText
を使用しています。
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コンポーネントツリーを人間が読める文字列に変換できます。言い換えれば、コンポーネントスナップショットは、テストの実行中に*生成*されたコンポーネントのレンダリング出力のテキスト表現です。次のようになります。
<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テストライブラリを使用すると、アプリの画面で要素を見つけて制御できます。たとえば、実際のユーザーと同じように、ボタンを*実際に*タップしたり、TextInput
にテキストを挿入したりできます。 vervolgens 、アプリの画面に特定の要素が存在するかどうか、表示されているかどうか、どのようなテキストが含まれているかなどをアサーションできます。
E2Eテストは、アプリの一部が機能しているという、可能な限り最高の自信を与えてくれます。トレードオフには以下が含まれます。
- 他のタイプのテストと比較して、作成に時間がかかる
- 実行速度が遅い
- 不安定になりやすい(「不安定な」テストとは、コードを変更せずにランダムに成功したり失敗したりするテストです)
アプリの重要な部分をE2Eテストでカバーするようにしてください。認証フロー、コア機能、支払いなど。アプリの重要でない部分には、より高速なJSテストを使用してください。テストを追加すればするほど、自信が高まりますが、テストの維持と実行に費やす時間も増えます。トレードオフを考慮して、最適なものを決定してください。
利用可能なE2Eテストツールはいくつかあります。React Nativeコミュニティでは、DetoxはReact Nativeアプリ向けに調整されているため、人気のあるフレームワークです。iOSおよびAndroidアプリの分野で他によく使われるライブラリは、AppiumまたはMaestroです。
まとめ
このガイドを読んで楽しんでいただければ幸いです。アプリをテストするには多くの方法があります。最初に何を使用するかを決めるのは難しいかもしれません。しかし、素晴らしいReact Nativeアプリにテストを追加し始めれば、すべてが理にかなってくるでしょう。さあ、何を待っているのですか?カバレッジを上げましょう!
リンク
このガイドは、元々Vojtech Novakによって完全に作成および寄稿されました。