自動テストとは
このユニットでは、自動テストの利点と、実行できるテストの種類について学習します。 また、適切なテストを行う方法についても学習し、使用できるテスト ツールをいくつか確認します。
自動テストとは
"自動テスト" ではソフトウェアを使用してコードを実行し、期待した結果と実際の結果を比較します。 ソフトウェアが期待どおりに機能することを確認するために、通常は人間がテスト計画で手順に従う、探査的テストまたは手動テストとこれを比較します。
手動テストには利点があります。 しかし、コード ベースのサイズが大きくなるにつれ、(エッジ ケースを含む) すべての機能を手動でテストするのは反復的で退屈なものになり、エラーが発生しやすくなります。 自動テストは、この負担の一部を排除し、手動テストの担当者が最も得意とすることに集中できるようにし、確実にユーザーがソフトウェアで有意義な体験ができるようにするのに役立つ場合があります。
テスト ピラミッド
自動テストについて考える場合、テストをレイヤーに分割するのが一般的です。 Mike Cohn は、彼の著書である Succeeding with Agile で、"テスト ピラミッド" と呼ばれる、この概念を提示しています。
これは Cohn のモデルの単純なバージョンですが、この概念は、関数、クラス、メソッドなどのソフトウェアの基本レベル (ピラミッドのコールアウト 1) を検証するテストを記述するためにほとんどの作業を行うことを示しています。 機能を組み合わせていくと、ユーザー インターフェイス (UI) レイヤー (ピラミッドのコールアウト 2) などで作業量が徐々に少なくなります。 考え方としては、より低いレベルの各コンポーネントが単独で期待どおりに動作することを確認できる場合、より高いレベルでのテストで必要なのは、複数のコンポーネントが連動して期待した結果が得られることを確認するだけになります。
テストをいつ記述する必要がありますか?
答えは主に、テストの記述に関するニーズと経験によって異なります。
既に記述して展開しているコードの追加テストを開始するのに遅すぎるということはありません。 これは、多くの場合、テスト チームのほとんどの作業を中断または必要とする機能に特に当てはまります。
継続的インテグレーションと継続的デリバリーのパイプラインにテストを関連付ける際に、耳にする 2 つの概念は "継続的テスト" と "シフト レフト" です。
継続的テストは、開発プロセスの早い段階と、すべての変更がパイプラインを移動するときにテストが行われることを意味します。 シフト レフトは、開発プロセスの早い段階でソフトウェアの品質とテストを考慮することを意味します。
たとえば、開発者は多くの場合、機能を開発し、パイプラインに変更を送る前にテスト スイート全体を実行するときに、テスト ケースを追加します。 この方法は、ビルドしている機能が期待どおりに動作し、既存の機能を中断しないようにするために役立ちます。
ここでは、Microsoft のクラウド アドボケイトである Abel Wang が、DevOps プランの品質を維持するために役立つ方法を説明する短いビデオをご覧ください。
Abel に聞く
シフト レフトでは多くの場合、テスト担当者が、機能のコードを記述する前でも、設計プロセスに関わる必要があります。 "ハンドオフ" モデルとこれを比較してみてください。ハンドオフ モデルでは、ソフトウェアが設計され、記述された後にのみ、テスト チームにテストする新機能が提示されます。 プロセスの後半でバグが検出されるとチームのデリバリー スケジュールに影響を与える可能性があり、開発者が最初に機能を構築してから数週間、場合によっては数か月後にバグが検出されることがあります。
トレードオフ
自動テストにはトレードオフがあります。 自動テストでは、テスト担当者はエンドユーザー エクスペリエンスの確認に時間を集中させることができますが、開発者はテスト コードの記述と保守により長い時間を費やす必要がある場合があります。
ただし、自動テストの要点は、テスト担当者が、最高品質のコード (期待どおりに機能することが実証されているコード) のみを受け取れるようにすることです。 したがって、開発者は、対応する必要があるバグがより少ないことと、当初は考慮していなかったエッジ ケースが原因でコードを書き直すことが避けられることで、時間を取り戻すことができます。
追加の利点
ドキュメントと、コードをより簡単にリファクタリングする機能は、自動テストの追加の 2 つの利点です。
ドキュメント
手動テストの計画は、ソフトウェアの動作方法と特定の機能が存在する理由に関する種類のドキュメントとして機能できます。
自動テストも同じ目的で機能できます。 自動テスト コードでは、多くの場合、人間が判読できる形式が使用されます。 指定した入力セットでは、ユーザーが入力する可能性のある値が表されます。 関連付けられている各出力では、ユーザーが期待する結果が指定されます。
実際に、多くの開発者は、新機能を実装する "前に" テスト コードを記述して、"テスト駆動開発" (TDD) 方式に従います。 考え方としては、最初に失敗する、一連のテスト (多くの場合、"仕様" と呼ばれる) を記述することです。 その後、開発者は、テストがすべて成功するまで、機能を実装するためのコードを段階的に記述します。 仕様で要件を文書化するだけでなく、TDD プロセスは、機能を実装するために必要な量のコードのみが確実に記述されるようにするのに役立ちます。
リファクタリング
リファクターでかなりの部分がより高速に実行されるようにする大規模なコード ベースがあるとします。 リファクタリング作業により、アプリケーションの一部が中断されないことをどのように把握しますか?
自動テストは、コントラクトの一種として機能します。 つまり、入力と予想される結果を指定します。 一連のテストが成功した場合、より適切にコードを試してリファクタリングできます。 変更を行うときに、必要なのはテストを実行し、引き続き成功することを確認することだけです。 リファクタリングの目標を達成したら、変更をビルド パイプラインに送信し、中断のリスクを低く保ちつつ、誰でも利点が得られるようにすることができます。
自動テストにはどのような種類がありますか?
自動テストには多くの種類があります。 各テストは別の目的を果たします。 たとえば、1 つのソフトウェアまたはその機能の 1 つに許可されているユーザーのみがアクセスできることを確認するのに役立つ、セキュリティ テストを実行する場合があります。
継続的インテグレーションとビルド パイプラインについて考える場合、通常は "開発テスト" について触れます。 開発テストは、アプリケーションをテスト環境または運用環境に展開する前に実行できるテストを指します。
たとえば、静的コード分析の一形式である "lint テスト" では、ソース コードがチームのスタイル ガイドに従っているかどうかを確認します。 一貫した形式のコードは、だれもが読みやすく保守しやすくなっています。
このモジュールでは、"単体テスト" と "コード カバレッジ テスト" を操作します。
単体テストでは、個々の関数やメソッドなど、プログラムまたはライブラリの最も基本的なコンポーネントを確認します。 期待される結果と共に 1 つまたは複数の入力を指定します。 テスト ランナーでは各テストを実行し、実際の結果と期待される結果が一致するかどうかを確認します。
たとえば、除算を含む算術演算を実行する関数があるとしましょう。 ユーザーがエッジ ケース値 (0 や -1 など) と共に入力することが予想される値をいくつか指定する場合があります。 特定の入力でエラーまたは例外が発生した場合は、関数で同じエラーが発生することを確認できます。
コード カバレッジ テストでは、単体テストでカバーされないコードの割合が計算されます。 コード カバレッジ テストには、関数がカバーされるように、コードに条件付きブランチを含めることができます。
コード カバレッジの割合が大きくなるほど、完全にテストされなかったコードで後でバグが見つからないことを確信できます。 100% のコード カバレッジに達する必要はありません。 実際には、開始時に割合が低くなる可能性がありますが、それは、問題があるか頻繁に使用されるコードをカバーするテストを、さらに追加できる開始点となります。
単体テストを分離させておく
単体テストについて学習する際に、"モック"、"スタブ"、"依存関係の挿入" などの用語を耳にすることがあります。
単体テストでは、複数のコンポーネントがどのようにやり取りしているかではなく、個々の関数やメソッドを確認する必要があることを思い出してください。 ただし、データベースまたは Web サーバーを呼び出す関数がある場合は、どのように対処すればよいでしょうか。
外部サービスへの呼び出しによって分離が損なわれるだけでなく、進捗が遅れる可能性もあります。 データベースや Web サーバーが停止した場合や、使用できない場合、その呼び出しによってテストの実行が中断される可能性もあります。
モック作成や依存関係の挿入などの手法を使用すると、この外部機能を模倣するコンポーネントを作成することができます。 このモジュールの後半で例を示します。
後で、"統合テスト" を実行し、アプリケーションが実際のデータベースまたは Web サーバーで正常に動作することを確認できます。
テストを適切に行うための要素
独自のテストを記述し、他のユーザーによって記述されたテストを読み取る経験を重ねるにつれ、適切なテストを識別できるようになります。 作業を開始するためのガイドラインをいくつか以下に示します。
- テストのためのテストは行わない: テストは、単にチェックを入れるためのチェックリストの項目ではなく、目的に適合している必要があります。 重要なコードが意図したとおりに動作し、既存の機能が中断されないことを確認するテストを記述します。
- テストが短時間で終わるようにする: 特に開発およびビルド フェーズで行われるテストは、可能な限りすぐに完了する必要があります。 各変更がパイプラインを移動するときにテストが実行される場合に、それらがボトルネックにならないようにします。
- テストが反復可能になるようにする: テストの実行で毎回同じ結果が生じる必要があります。ご自分のコンピューターで実行するか、同僚のコンピューターで実行するか、またはビルド パイプラインで実行するかは関係ありません。
- 焦点を絞ったテストにする: テストは他のユーザーによって記述されたコードをカバーするためのものであるとよく誤解されています。 通常、テストでは自分のコードのみをカバーする必要があります。 たとえば、プロジェクトでオープンソースのグラフィックス ライブラリを使用する場合、そのライブラリをテストする必要はありません。
- 正しい細分性を選択する: たとえば、単体テストを実行する場合、個々のテストで複数の関数やメソッドを組み合わせたり、テストしたりする必要はありません。 各関数を個別にテストし、複数のコンポーネントが正しくやり取りしていることを確認する統合テスト を後で記述します。
使用できるテスト ツールの種類
使用するテスト ツールは、ビルドするアプリケーションの種類と、実行するテストの種類によって異なります。 たとえば、Selenium を使用して、さまざまな種類の Web ブラウザーとオペレーティング システムで UI テストを実行することができます。
アプリケーションがどの言語で記述されているかに関係なく、使用できる多くのテスト ツールがあります。
たとえば、Java アプリケーションでは、lint テストを実行するために Checkstyle を選択し、単体テストを実行するために JUnit を選択する可能性があります。
このモジュールでは単体テストに NUnit を使用します。.NET コミュニティでこれが一般的であるからです。