最近、昔作ったTest Doubleの解説資料を参照・引用してくれる方がちらほら出ていて恐縮しているのですが、見直してみると結構わかりにくい資料なので今回文章としてまとめたいと思います。内容は世間一般的に言われているMock、Stub、Fake、Dummyといった言葉の定義になります。
Test Doubleとは
Test Doubleとは、テスト実行時に、テスト対象が依存しているコンポーネントと置き換わるものです。「テスト代役」と訳されることもあります。世の中でMock、Stub、Fake、Dummyなどと呼ばれているものの総称に位置づけられます。
簡単な例を以下に示します。このコードでは、テストメソッド「テストコード()」がメソッド「テスト対象()」をテストしています。また「テスト対象()」は、中でメソッド「外部メソッド()」を実行しています。なお「外部メソッド」はテスト対象でないとします。
@Test public void テストコード() { AssertEquals(期待値, テスト対象()); } int テスト対象() { ... 外部メソッド(); ... }
上記の「外部メソッド()」のようなテスト対象の依存要素は、例えば以下の理由でテスト中に実行したくないことがあります。
- 実行上の制約を持つ。例えば「テスト環境にないハードウェアを制御する」「勝手に変更してはいけない顧客のDBを操作する」「実行にコストがかかる」「OSや外部ライブラリのAPIであり挙動をコントロールできない」といった制約を持つ。
- テストのために自由に操作したい。例えば『「外部メソッド()」から例外を投げて正しく動くかチェックする』『「外部メソッド()」が呼び出されたかチェックする』のように操作や検証に活用したい
こうした状況に対応するためには、「外部メソッド()」をテスト実行時に都合の良い代替と置き換えないといけません。Test Doubleとは、その「外部メソッド()」と置き換わるものが該当します。
図にすると以下のような感じです。本番環境での依存コンポーネントを、テストの際はTest Doubleに置き換えていきます。
※今回は入門用のテキストということで、コードをかなり簡略化して書いていきます。実際はTest Doubleは一般的にオブジェクトであり、Dependency InjectionやDependency Lookupといった仕組みで本物と置き換えます。詳細は「Test Double Patterns at XUnitPatterns.com」を参照ください。
xUnit Test PatternsのTest Doubleパターン
このTest Doubleの定義や分類例には、有力なものにユニットテストの実装パターン集であるxUnit Test Patterns(index at XUnitPatterns.comおよび同名の書籍)があります。そこではTest Doubleを用途に応じて以下のように分類しています。
- Test Double Pattern
- Test Stub
- Test Spy
- Mock Object
- Fake Object
- (Dummy Object)
具体的には、Test Doubleに含まれる「Test Stub」「Test Spy」「Mock Oject」「Fake Object」の4つ+Test Doubleに類似した「Dummy Object」の1つの計5つに分類しています。
なおTest Doubleの定義はいろいろな流儀があり、このxUnit Test Patternsの定義がデファクトスタンダードというわけではありません。ただ分類が明快なほか、Martin Fowlerや id:t_wada さんなどユニットテストの世界で有力な技術者が理解を示しているので、個人的に推奨できる分類と判断しています。それぞれの詳細は後述していきます。
前提:間接入力と間接出力
なお上記の5つは、おもに間接入力と間接出力の扱いに応じて分類されています。具体的な説明に入る前に、その間接入力と間接出力について説明します。
間接入力
まず間接入力はテストコードから見えないテスト対象への入力です。
コードでの例を示します。ここではメソッド「テスト対象()」が中で「外部メソッド()」を呼び、その戻り値を使用しています。
int テスト対象() { ... answer = 外部メソッド(); ... } @Test public void テストコード() { AssertEquals(期待値, テスト対象()); }
ここでの「外部メソッド()」の戻り値のように、テストコードから直接見えないがテスト対象に影響を与える入力が、間接入力となります。なお間接入力には、例えばテスト対象が依存するオブジェクトからの例外発生も含まれます。
間接出力
一方間接出力はテストコードから見えないテスト対象の出力です。
コードの例を示します。テスト対象が中で「外部メソッド()」を呼んでいますが、そこで「外部メソッド()」に引数を出力しています。
int テスト対象() { ... 外部メソッド(x); ... } @Test public void テストコード() { AssertEquals(期待値, テスト対象()); }
ここでの「外部メソッド()」への引数xのように、テストコードから直接見えないが、テスト対象が外に出力しているものが、間接出力となります。なお間接出力には「外部メソッド()が実行されたか」「複数のメソッドが順番通り呼ばれたか」のようなメソッド呼出しの有無も含まれます。
間接出力・間接入力の関係図は以下のようになります。
Test Double詳細
では間接入力・間接出力の定義を踏まえて、各Test Doubleの詳細を説明します。
Dummy Object
Dummy Objectはテストに影響を与えない代替オブジェクトです。
かなり極端ですが、コードの例を以下に示します(変なコードですがとりあえずTDDでの仮実装中とでも考えてください)。
int テスト対象(Foo foo) { return 10; } @Test public void テストコード() { Foo foo = new Foo();//Dummy Object AssertEquals(期待値, テスト対象(foo)); }
上記の例では、「テスト対象()」は引数を持ちますが、出力である戻り値「return 10;」と引数は全く無関係です。そこに指定される「foo」はテストに影響を与えないという点で、まさにDummy Objectに該当します。
Test Stub
テスト対象への間接入力を操作するTest Doubleは、Test Stubと分類されます。テストでは、任意の間接入力がテスト対象に出力されるように、間接入力をTest Stubに事前設定して使用します。例えば以下のような感じです。
//Test Stubとして動作する代替関数 int 外部メソッド() { return 間接入力値; } int テスト対象() { ... answer = 外部メソッド(); ... } @Test public void テストコード() { 間接入力値 = 100; //外部メソッド()がテストにとって望ましい値をテスト対象()に返すようセット AssertEquals(期待値, テスト対象()); }
上記のコードでは、「外部メソッド()」による間接入力「間接入力値」を、テストコード上で事前設定してからテストを実行しています。そのように望ましいように間接入力を操作できるオブジェクトが、Test Stubです。
Test Spy
テスト対象の間接出力を記録し、それをテストコードから参照可能にするTest Doubleは、Test Spyと分類されます。テストでは、テスト対象の間接出力を記録させ、その後テストコード上でその間接出力を検証します。なお間接入力を操作することもあります。
例えば以下のような感じです。
//Test Spyとして動作する代替関数 void 外部メソッド(int input) { 間接出力値 = input; } int テスト対象() { ... 外部メソッド(x); ... } @Test public void テストコード() { テスト対象(); AssertEquals(期待値, 間接出力値);//Test Spyに記録させておいた間接出力値を比較検証する }
上記のコードでは、「外部メソッド()」への間接出力をいったん「間接出力値」に保持します。テストコードはテスト実行後それを参照することで、間接出力が適切だったかチェックします。こうした間接出力を保持するものがTest Spyとなります。
Mock Object
以下の用途をあわせもつTest Doubleは、Mock Objectと分類されます。
- テスト対象の間接出力の期待結果を持っています。
- テスト対象を実行している間、テスト対象の間接出力を取得します。
- 間接出力を確保できたら、Mock Objectの中でその期待結果と比較検証し、成功か失敗か判定します。
- テストコードは、テスト対象を実行後、Mock Objectから検証の成功・失敗の情報を受け取ります。
- Test Stubの機能を包含していることもあります。
非常に簡略化した例ですが、例えば以下のようなものです。
//Mock Objectとして動作する代替関数 void 外部メソッド(int input) { if (期待する間接出力値 == input) { テスト成功フラグ = true; return; } テスト成功フラグ = false; } int テスト対象() { ... 外部メソッド(x); ... } @Test public void テストコード() { 期待する間接出力値 = 999; //Mock内で比較に用いる期待値 テスト対象(); AssertTrue(テスト成功フラグ); //Mock内で検証した結果をチェック }
ここでは、「外部メソッド()」が、テスト対象から間接出力を受け取るとともに、同時にその検証も行っています。そしてテストコードでは、その検証結果を見てテスト結果を決めています。このように間接出力の検証能力を持つのがMock Objectです。
なおMock ObjectとTest Spyは両方とも間接出力を検証するためのTest Doubleです。ただ「Mock ObjectはMock Object内で間接出力結果を評価する」のに対し、「Test Spyは間接出力を保持するだけで、間接出力結果の評価は後からテストコード上で行う」という違いがあります。
Fake Object
テスト実行中、代替元の本物と同じように動けるTest DoubleはFake Objectと分類されます。Fake Objectは間接出力を受け取り、間接入力を操作しますが、あくまでそれも本物と同じように処理したり出力したりします。
補足
説明は以上ですが、おまけの補足解説をいくつか行いたいと思います。
分類方法
Test Doubleの分類方法は以下のような感じです。
- テストの範囲内で本物と同じように動作するTest DoubleはFake Object。
- 内部のパラメータや状態がなんでもあってもテストに影響を及ぼさない代替オブジェクトなら、Dummy Object
- 上記以外で、テスト対象の間接出力を受け取り、かつ自身でその検証を行うTest DoubleはMock Object
- 上記以外で、テスト対象の間接出力を受け取りそれをあとから参照可能にするTest DoubleはTest Spy
- 上記以外で、テスト対象の間接入力を操作できるTest DoubleはTest Stub