エンジニアリングApr 4, 201913 分 READ

ソフトウェア テストの手法 - パート 1: モッキング、スタビング、コントラクト テスト

TestDefs

私の前回の記事では、ソフトウェアが本番稼働に至るまでの体系的なプロセスについて紹介し、どのタイプのテストを、どのタイミングで実施すべきか、その理由と共に説明しました。今回は、各タイプのテストの具体的な実施方法について解説していきます。

各テスト レイヤーの実施に役立つ、モッキングとスタビングの手法やテスト駆動開発を取り上げます。その前に、前回の記事で紹介した「テスト ピラミッド」のコンセプトをおさらいしておきましょう。これを知っていると、タイプ別のテストの違いと、効果的な実施タイミングを理解しやすくなります。

単体テストまたはコンポーネント テスト (下図で示すピラミッドの最下層) は、低コストですばやく実行できるテストです。この種のテストを、できるだけ多く実施するようにしましょう。単体テストをすべてやり尽くした場合にのみ、結合テストや UI レイヤー テストなどの、(時間とリソースの両方の面で) より高コストなテストに移るようにしてください。

ピラミッドの最下層

このシリーズでは、すべての開発者が取り揃えておくべきテストツールを取り上げ、各ツールを使用するタイミングとその理由、具体的な使い方を見ていきたいと思います。また、先ほどのピラミッドの各テストの実施方法を説明すると共に、モッキング、スタビング、コントラクト テストのコンセプトについても解説します。さらに、本シリーズのパート 2 では、テスト駆動開発 (TDD) と振る舞い駆動開発 (BDD) について取り上げます。

モッキングとスタビング

多くの方が、モッキングとスタビングは単体テストやコンポーネント テストのみで使用されると考えていらっしゃるようですが、今回はモック オブジェクトやスタブが他のテスト レイヤーにも役立つところをお見せしたいと思います。

まずは言葉の定義から確認しましょう。

モッキングとは、テストのスピードと信頼性を高めるために、実際の外部サービスや内部サービスの代わりをする偽のサービスを作成することです。実装をテストするときに、オブジェクトの関数や振る舞いではなくプロパティが必要となる場合、モックを使用できます。

スタビングも、モッキングと同じく実物の代わりを務めるオブジェクトを作成することを指しますが、スタブの場合はオブジェクト全体ではなく、振る舞いのみを代替します。スタブを使用するのは、実装のテストに、オブジェクトの所定の振る舞いのみが必要な場合です。

モックとスタブの違いについては、ブログ記事 https://martinfowler.com/articles/mocksArentStubs.html にわかりやすく説明されていて、参考になります。

それでは、こうした手法を適用し、先ほどのピラミッドの全レベルでテストの性能を高める方法を見ていきましょう。

単体 & コンポーテント テストでのモックとスタブの活用

外部機能のモッキング

作成したコードで、システムコールやデータベースへのアクセスといった外部の依存関係を使用する場合には、モックやスタブの利用をお勧めします。たとえば、テストを行うときには必ず、対象の実装を実行しなければなりません。その際に delete 関数や create 関数を実行すると、実際にファイルが作成されたり、削除されたりします。この作業は非効率的であるだけでなく、作成または削除されるデータは実質的には役に立ちません。さらに、テストのたびに作成されたデータを手作業で削除しなければならないので、環境を元に戻すために多くのコストがかかります。こうした状況で便利なのがモッキングとスタビングです。

モックやスタブを外部機能の代替として使用すれば、依存関係のないテスト環境を構築できます。仮に、テストを実行すると /tmp/test_file.txt というファイルが書き込まれ、テストを実行中のシステムによって、そのファイルが削除されるとしましょう。ここで問題になるのは、テストが外部機能に依存していることではありません。システム コールに時間がかかることです。この例では、ファイル システムの呼び出しに対する応答をスタブで代替し、応答を即座に返すことにより、テスト時間を大幅に短縮できます。

モックやスタブのもう 1 つの利点は、複雑なシナリオを簡単に再現できる点です。たとえば、ファイルシステムから受け取る可能性のある多数のエラー応答をテストするために、該当する状況を実際に再現することが、はるかに容易になります。破損したファイルを削除したい場合を考えてみましょう。プログラムを通じて破損ファイルを作成するのは、必ずしも簡単ではありません。ただし、破損ファイルと関連付けられたエラーコードを返すだけなら、スタブが返す値を変更しさえすれば済みます。

次のサンプルコードをご覧ください。

def read_and_trim(file_path)
	return os.open(file_path).rstrip("\n") #method will call system call to look for the file from the given file path and read the content from them and removing new line terminator.

前述のコードでは Python に標準で組み込まれている open 関数を使用し、システムコールを呼び出すことで、所定のファイルパスで目的のファイルを検索できます。この関数のテストを実行すると、いついかなる場合でも次の条件が適用されます。

  1. テストで検索対象となるファイルが必ず存在していなければなりません。該当ファイルが存在しない場合、テストは失敗します。
  2. このテストではシステムコールの応答を待つ必要があります。システムコールがタイムアウトすると、テストは失敗します。

どちらのケースの失敗でも、対象の実装がジョブの実行に失敗します。この場合のテストには独立性がないだけでなく (システムコールの応答に依存しているため)、効率性にも劣ります (システムコールへの接続には、要求と応答の受け渡しに時間がかかるため)。

この実装のテストコードは、次のようになります。

@unittest.mock.patch("builtins.open", new_callable=mock_open, read_data="fake file content\n")
def test_read_and_trim_content(self, mock_object):
    self.assertEqual(read_and_trim("/fake/file/path"), "fake file content")
    mock_object.assert_called_with("/fake/file/path")

ここでは、Python の mock.patch を使用して、組み込みの open 関数の呼び出しをモックに差し替えています。こうすると、実際に作成した実装のみをテストすることができます。

わかりやすい例をもう 1 つ挙げてみましょう。単体テストにおけるデータベースの呼び出しにも、モックとスタブを活用できます。作成した関数によってデータベースからエンティティが削除されることをテストするとします。1 回目のテストでは、削除するためのエンティティとして、皆さんが手動でファイルを作成することで、テストが成功しました。ところが、2 回目のテストは (皆さん以外の) 他の担当者が行ったため、エンティティを手動で作成しておく必要があるとは知らず、テストが失敗しました。つまり、このテストには独立性が欠けていると言えます。

こうしたケースでは、ファイルの削除を実行するにあたって、データを修正したり、オペレーティングシステムを呼び出したりする必要性を解消しなければなりません。そうすれば、だれかがテストデータの作成をうっかり忘れるたびに、テストの信頼性が損なわれるのを防ぐことができます。

内部関数のモッキングとスタビング

モックとスタブは単体テストに大いに役立ちます。機能や実装を外部に依存することなくテストできると同時に、前回の記事で述べたように、効率的かつ低コストで単体テストを実行できるからです。

単体テストやコンポーネント テストの中でも、モックとスタブが適しているのは、実装をテストするために他のメソッドやクラスとの情報のやり取りが発生する場合です。つまり、テスト対象の実装が情報を受け渡す場合に、相手がクラス オブジェクトならモックで、メソッドの振る舞いならスタブで代用します。他の機能やクラスをモックやスタブで代用し、実装のロジックのみをテストできることは、単体テストを実施するうえできわめて有益であり、テストの効果を最大限に高められます。

注: テストはコードに合わせて更新していきましょう。単体テストは、機能の全体的な実用性よりも実装の細部に焦点を当てているため、時間と共に変化することが最も多いテストです。そのため、テスト内に多くのモックデータを使用する場合には、コードの改良に応じてモックも更新していくことが必要になります。これを怠ると、システム内の思わぬバグにつながる可能性があります。一度記述したテストは、ずっと使えるとは限りません。コードの変更やリファクタリングを行ったら、必ずテストをメンテナンスし、コードの変更内容をテストに反映するようにしてください。

結合テストでのモックの活用

結合テストではサービス間の関係をテストします。この場合、依存関係にあるすべてのサービスを、テスト環境のためにセットアップして実行するのも選択肢の 1 つですが、これは不必要なアプローチです。というのも、自分でコントロールできないサービスを利用すると、さまざまな外部要因によってテストが失敗する可能性があり、余計な時間がかかったり、テストの複雑性が増したりします。代わりに、モックやスタブを使用してサービスの結合テストをいくつか作成し、テスト環境をコンパクトにすることをお勧めします。その結果、テストスイートの信頼性がいかに向上するかを説明していきましょう。

結合テストは単体テストとは勝手が違います。結合テストを実施できるのは、皆さんが編集権限を持っている実装や機能のみです。こうした目的でもモックとスタブは使用できます。まずは、サービス間の重要な結合箇所を見極めてください。次に、モックで代用可能な外部または内部のサービスを判断しましょう。

たとえば、下の例のように、コードが GitHub API から情報を取得するとします。GitHub API の応答方法を個人的に変更することはできないため、その点についてテストする必要はありません。期待する GitHub API の応答をモッキングすれば、内部コード ベース内での情報の受け渡しのテストに、より多くの時間を割けるようになります。

@unittest.mock.patch('Github')
def test_parsed_content_from_git(self, mocked_git):
   expected_decoded_content = "b'# Sample Hello World\n\n> How to run this app\n\n- installation\n\n dependencies\n"
   mocked_git.get_repo.return_value = expected_decoded_content
   parsed_content = read_parse_from content(repo='my/repo',
                                            file_to_read='README.md')
   self.assertEqual(parsed_content['titles'], ['Sample Hello World'])

上記のテストコードでは、GitHub API を呼び出して JSON オブジェクトをパースするクラスと、read_parse_from_content メソッドが連携されています。このテストの目的は、2 つのクラス間の連携をテストすることです。

上記のテストではモックを使用しているため、GitHub API を実際に呼び出す必要がなく、テストのスピードアップと依存性の軽減に貢献しています。また、モックを使用することで、テストの実行環境からインターネットにアクセスしなくて済むため、時間の節約と手間の解消にもなっています。ただし、依存関係にある外部サービスをモッキングしつつ、信頼性の高いテストを実行するには、外部の依存サービスが実際の環境でどのように動作するかを理解しておくことがきわめて重要です。たとえば、先ほどのサンプルコード内にある expected_decoded_content の動作と、GitHub が実際にリポジトリ ファイルの内容を返す方法が異なっていたとしましょう。モック テストで想定した条件が正しくないと、予想外のバグにつながる可能性があります。外部サービスの応答をモックで代替してテストを記述する場合には、依存関係にある外部サービスについて実際の呼び出しのスナップショットを作成しておき、それをモックとして使用するのが最善の方法です。スナップショットを使用して応答のモックを一度作成したら、API には後方互換性がある場合がほとんどなので、モックの頻繁な変更は必要ないはずです。ただし、たまに予想外の変更が行われることもあるため、定期的に API を検証することが大切になります。

2019-04-04-image2.png

2019-04-04-image3.png

コントラクト テストでの (マイクロサービス アーキテクチャにおける) モックとスタブの活用

2 つの異なるサービスを連携させる場合、両サービスにはそれぞれに「期待される動作」があります。言い換えれば、どのような要求に対し、何が返されるかが規定されているわけです。こうした規定は、連携されるエンドポイント間の契約 (コントラクト) だと考えることができます。こうした規定があることから、コントラクト テストを実施することでサービス間の連携をテストできます。

わかりやすいように例を挙げてみましょう。先ほど述べたように、バージョンタグの付いた API はあまり変更されないのが一般的で、一度も変更されない可能性もあります。どの API を選択する場合でも、通常はその API に関するドキュメントが提供されており、どのような機能があるかを確認できます。特定のバージョンの API を使用することに決めると、その API を呼び出すことで信頼できる戻り値を得られます。これが、API を提供するエンジニアと、そのデータを使用するエンジニアとの間で当然のこととされるコントラクトです。

こうしたコントラクトの考え方は、内部サービスのテストにも適用できます。マイクロサービス アーキテクチャを使用した大規模なアプリケーションでは、テストのためにシステムとインフラストラクチャの全体を準備すると、コストがかかりすぎる場合があります。こうしたアプリケーションには、コントラクト テストの使用が非常に効果的です。先ほどのテスト ピラミッドで、コントラクト テストは単体 & コンポーネント テストのレイヤーと結合テスト レイヤーの間に位置しますが、コントラクト テストの範囲をどこまでに定めているかは、企業によって異なります。中には、エンドツーエンドのテストまたは機能テストをコントラクト テストで完全に置き換えている企業もあります。

コントラクトに基づくテストは、2 つの重要な役割を果たします。

  1. 事前に合意されたエンドポイントとの接続を確認する
  2. 所定の引数を使用し、エンドポイントからの応答を確認する

気象予報アプリケーションを例に考えてみましょう。このアプリケーションでは、気象サービスとユーザー サービスとの間で情報がやり取りされます。ユーザー サービスが日付情報と共に気象サービスのエンドポイントに接続 (要求を送信) すると、ユーザー サービスが日付情報を処理し、該当日の気象データを取得します。この 2 つのサービスのコントラクトとは、気象サービス側でユーザー サービスがいつでもアクセス可能なエンドポイントが維持され、ユーザー サービスの要求に従って常に同じ形式の有効なデータが提供されることです。

では、モックとスタブを活用して、このコントラクトをテストする方法を見ていきましょう。この例では、ユーザー サービスから気象サービスに対して実際にデータを要求する代わりに、気象サービスからの応答をモックで代用することができます。2 つのサービス間にはコントラクトがあるため、エンドポイントと応答は基本的に変更されないからです。この方法でテストを行えば、双方のサービスでお互いに対する依存関係を解消できるため、テストのスピードと信頼性が向上します。

前回の記事では、さまざまなテストをそれぞれ異なる環境で実行することについて説明すると共に、同じテストを異なる構成と異なる環境で実施することが有効な場合もあることを紹介しました。コントラクト テストは、後者のケースの代表的な例です。構成と環境を変えてコントラクト テストを実行してみると、さまざまな目標を達成できます。開発や継続的インテグレーション (CI) といった下位レイヤーでは、コントラクトをモックで代用してテストを行うことで、外部の影響を受けずに、その環境の中だけで内部の実装をテストするのに役立ちます。一方、QA やステージングといった、より上位の環境では、コントラクトをモックで代用せず、依存関係にある外部サービスに実際に接続して同じテストを実行するとよいでしょう。ここで説明したようなコントラクト テストや応答のモッキングを行うには、Mbtest のようなツールを利用すると便利です。

2019-04-04-image4.png

ここまで、さまざまなテストレイヤーでのモックとスタブの活用例を見てきました。最後に、モックとスタブのメリットをまとめておきます。

  1. モックとスタブを活用するとテストの実行時間が短くて済みます。これは、外部サービスに接続する必要がなく、応答の待ち時間が発生しないためです。
  2. 自分でコントロールおよび変更可能な箇所のみに対応するよう、テストの範囲を柔軟に設定できます。実際の外部サービスに接続した場合、外部サービスに不具合があっったりテストが失敗したりしても、皆さんにはどうすることもできません。モッキングを行えば、皆さんの影響が及ぶ範囲でのみテストを実行でき、自力で解決できない問題の発生を回避できます。
  3. 外部 API の呼び出しをモッキングすると、テストの信頼性が向上します。
  4. コントラクト テストではサービスチームによる開発の自律性が促進されます。

本シリーズのパート 2 では、テスト駆動開発 (TDD) と振る舞い駆動開発 (BDD) の原則と共に、機能テストから単体テストまであらゆるテストの結果を改善するうえでこうした原則がいかに役に立つかを説明したいと思います。

関連記事

クリップボードにコピー