今回はダミーのオブジェクト(モックオブジェクト)を使ったテストについて見ていきます。
モックオブジェクトを使ったテスト
さて、折角完成したCartクラスですが、商品コードの代わりに商品クラス(Itemインターフェースを実装したクラス)を導入し、さらに商品の合計代金も取得できるようにすることになりました。ありがちな話ですね。具体的には、次のようなItemインターフェースが提供されています。商品コードのほかに、商品名や価格が取得可能なようです。
しかし、肝心の実装クラス(ItemImplクラス)自身はまだ作成されていません。こういった場合、どうすればいいでしょうか?ItemImplクラスが作成されるまで待ちますか?確かに、簡単なクラスであればそれもあり得るかも知れません。しかし、対象となっているクラスが複雑な場合や時間的な制限がある場合、そうも言っていられませんね。可能であれば、Cartクラスの修正やテストを進めておきたいところです。
幸い、PHPUnit3には「モックオブジェクト」を生成する機能があります。「モック(Mock)」には「模造品」「偽の」といった意味がありますが、これを使うことで「まだ存在しない」クラスのインスタンスを使ったテストが可能になります。
次に、PHPUnitで提供されているモックオブジェクトの生成機能について説明します。
PHPUnit3のモックオブジェクト生成機能
PHPUnitはPHPのリフレクション機能を使ってモックオブジェクトを生成します。また、生成されるオブジェクトにメソッドを定義したり、そのメソッドの振る舞いを指定することができます。さらに、「制約」と呼ばれるクラス(PHPUnit_Framework_Constraintのサブクラス)を使って、期待する回数だけメソッドがコールされたかどうかや、期待した振る舞いをしているかどうかのテストも可能です。
それでは、具体的な生成手順を見ていくことにします。
モックオブジェクトの生成は、PHPUnit_Framework_TestCaseクラスのgetMockメソッドを呼び出すだけです。getMockメソッドのAPIとパラメータについては、以下のようになっています。
パラメータ | 必須 | 意味 |
---|
$className | ○ | このクラスのモックオブジェクトを生成する |
array $methods | | 生成するメソッド |
array $arguments | | コンストラクタの引数 |
string $mockClassName | | モックオブジェクト自身のクラス名。デフォルトは、Mock_[$className]_[生成時刻(ミリ秒)のハッシュ値] |
最も簡単なコードは以下のようになります。
このコードを実行すると内部では以下のようなコードが生成され、そのインスタンスが返されます。以下は、生成されるモックオブジェクトのソース例です。
お気づきの通り、実際に生成されるオブジェクトは、$classNameで指定したクラスのサブクラスとなります。
メソッドを指定してモックオブジェクトを生成する場合、getMockメソッドの第2引数にメソッド名を配列で指定します。
内部で生成されるコードは以下のようになり、第2引数で指定したメソッドが生成されていることが分かると思います。以下は、生成されるモックオブジェクトのソース例です。
また、既存のクラスやインターフェースを継承・実装したモックオブジェクトを生成することもできます。この場合のPHPコードは
この場合、第4引数にモックオブジェクトの名前を指定していますので、AnotherItemオブジェクトが生成されることになります。なお、前述の通り、モックオブジェクトは指定したクラスのサブクラスになりますので、finalクラスを使ってモックオブジェクトを生成することはできません。また、ItemクラスもしくはItemインターフェースで定義されているメソッドは、モックオブジェクト側でオーバーライドされます。
PHPUnit3のモックオブジェクト機能は、オブジェクトを生成するだけではなく、その振る舞いも任意に変更することが可能です。モックオブジェクトのメソッドの振る舞いを変更するには、
- 実行回数の制約を設ける
- メソッド名を指定する
- 具体的な振る舞いを記述する
を指定する必要があります。
1つ目の「実行回数の制約」には、たとえば「1回のみ呼び出される」や「0回以上呼び出される」といったものを指定します。具体的には、PHPUnit_Framework_MockObject_Matcher_Invocationインターフェースを実装したクラスのインスタンスを指定します。具体的なクラスについては、PHPUnitポケットガイドを参照してください。
なお、指定した制約を満たさない場合、テストに失敗したと見なされます。
また、メソッドの振る舞いには、
のいずれかが指定可能で、PHPUnit_Framework_MockObject_Stubインターフェースを継承したクラスです。具体的なクラスについては、PHPUnitポケットガイドを参照してください。
以下、サンプルコードになります。
また、実行回数以外に、期待した振る舞いをするかをテストするための制約も用意されています。これらの制約は、willメソッドの代わりにwithメソッドを使用し、その引数として利用可能です。
以下、サンプルコードになります。
具体的な制約クラスについては、PHPUnitポケットガイドを参照してください。
CartTestクラスにモックオブジェクトを導入する
それでは、CartTestクラスにモックオブジェクトを導入してみましょう。まず、前述の通り、次のようなItemインターフェースが提供されています。
また、Itemインターフェースを導入することで、Cartクラスのaddメソッドの引数が変更になることとします。
さらに、商品の合計代金を取得するgetTotalメソッドを追加します。
まずCartTestクラスから修正していきます。CartTestクラス内でCartクラスのaddメソッドを使用している箇所を見てみると、3種類の商品を扱っている事が分かりますので、3種類のモックオブジェクトを生成するようにしましょう。今回は、商品コード毎にモックオブジェクトを生成するprivateメソッドを追加しました。以下、CartTest.phpの抜粋です。
そして、addメソッドの引数を、先のメソッドを呼び出すコードに変更します。
また、Itemクラスを導入することで、Cartクラス内部に保持している商品情報の持たせ方も変更する必要がありそうです。これまでは、キーとして商品コード、値として数量を持つハッシュでしたが、これを
- 第1キーとして商品コード
- 第2キーとして文字列「amount」もしくは「object」
- 第2キーが「amount」の場合、値として数量
- 第2キーが「object」の場合、値としてItemオブジェクト
に変更します。具体的には、以下のようなハッシュとなります。
これに伴い、テスト側も修正します。
最終的なCartTestクラスは以下のようになりました。
一方のCartクラス側ですが、getTotalメソッドを用意します。
それでは、テストを実行してみましょう。
当然、テストに失敗します。それでは、Cartクラスを修正していきましょう。変更点は、以下のとおりです。
- addメソッドのシグネチャ(引数)の変更
- addメソッドで、商品コードをItemオブジェクトのgetCodeメソッドを使って取得する
- getTotalメソッドを実装する
- 内部ハッシュの変更とそれに伴う修正
修正後のCartクラスは、次のようになりました。
再度テストを実行します。
少し大きめの修正でしたが、すべてのテストに成功し、問題ないことが確認できました。
次回は、PHPUnit3の便利な機能とPHP版プロジェクトビルドシステムであるPhingとの連携について見ていきます。