PHPUnit:モックオブジェクト

多くの場合、単体テストを作成するとき、テスト対象のクラスが外部ソースからのデータに依存しているという事実に対処する必要があります。外部ソースは状態を制御できません。 そのようなソースには、広範囲にわたる公開データベースまたはサービス、物理プロセスのセンサーなどが含まれます。または、特定のアクションが厳密に定義された順序で実行されることを確認する必要があります。 これらの場合、モックオブジェクトが役立ちます(モックは英語のパロディです)。外部の依存関係から分離してクラスをテストできます。 PHPUnitでのMockオブジェクトの使用は、この記事専用です。



使用例として、次のクラスの説明を使用します。

class MyClass { protected function showWord($word) { /*       */ } protected function getTemperature() { /*     */ } public getWord($temparature) { $temperature = (int)$temparature; if ($temperature < 15) { return 'cold'; } if ($temperature > 25) { return 'hot'; } return 'warm'; } public function process() { $temperature = $this->getTemperature(); $word = $this->getWord($temperature); $this->showWord($word); } }
      
      





このクラスのオブジェクトは、周囲の温度に応じて、3つの気象条件の1つを1つのデバイスに表示するように設計されています。 コードを書いている時点では、結果を表示するデバイスも温度センサーも使用できず、それらにアクセスしようとすると、プログラムが失敗する可能性があります。



最も単純なケースでは、ロジックを検証するために、指定されたクラスから継承し、接続されていないデバイスにアクセスするスタブメソッドに置き換え、子孫インスタンスでユニットテストを実行できます。 PHPUnitのモックオブジェクトはほぼ同じ方法で実装されますが、組み込みAPIの形式で追加の利便性が提供されます。



モックオブジェクトの取得



Mockオブジェクトのインスタンスを取得するには、getMock()メソッドを使用します。

 class MyClassTest extends PHPUnit_Framework_TestCase { public function test_process() { $mock = $this->getMock('MyClass'); // ,   $mock    MyClass $this->assertInstanceOf('MyClass', $mock); } }
      
      





ご覧のとおり、必要なMockオブジェクトの取得は非常に簡単です。 デフォルトでは、その中のすべてのメソッドは、何もせず常にnullを返すスタブに置き換えられます。



GetMock呼び出しオプション


 public function getMock( $originalClassName, //   ,     Mock  $methods = array(), //           array $arguments = array(), // ,    $mockClassName = '', //    Mock  $callOriginalConstructor = true, //   __construct() $callOriginalClone = true, //   __clone() $callAutoload = true //   __autoload() );
      
      





getMock()をnullの2番目の引数としてビルダーに渡すと、Mockオブジェクトが置換なしで返されます。



getMockBuilder


チェーンスタイルで記述することを好む人のために、PHPUnitは適切なコンストラクタを提供します。

 $mock = $this->getMockBuilder('MyClass') ->setMethods(null) ->setConstructorArgs(array()) ->setMockClassName('') //   ,   Mock  "" ->disableOriginalConstructor() ->disableOriginalClone() ->disableAutoload() ->getMock();
      
      





チェーンは常にgetMockBuilder()メソッドで開始し、getMock()メソッドでダウンロードする必要があります-これらはチェーン内で必要な唯一のリンクです。



モックオブジェクトを取得する追加の方法




これはすべて素晴らしいです-あなたは言いますが、次は何ですか? これに応えて、私たちは最も興味深いことにたどり着いたと言います。



メソッド呼び出しを待っています



PHPUnitを使用すると、置換されたメソッドの呼び出しの数と順序を制御できます。 これを行うには、expects()コンストラクトを使用し、続いてmethod()を使用して目的のメソッドを使用します。 例として、記事の冒頭のクラスを見て、それについて次のテストを書いてみましょう。

 public function test_process() { $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord')); $mock->expects($this->once())->method('getTemperature'); $mock->expects($this->once())->method('showWord'); $mock->expects($this->once())->method('getWord'); $mock->process(); }
      
      





process()メソッドを呼び出すときに、リストされている3つのメソッドgetTemperature()、getWord()、showWord()を1回呼び出すと、このテストの結果が成功します。 テストでは、showWord()の呼び出しがチェックされた後にgetWord()の呼び出しがチェックされますが、テストメソッドでは逆のことが当てはまることに注意してください。 そうです、ここにエラーはありません。 PHPUnitでメソッド呼び出しの順序を制御するために、別の構成(at())が使用されます。 PHPUnitがメソッド呼び出しのシーケンスを同時にチェックするように、テストコードを少し修正しましょう。

 public function test_process() { $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord')); $mock->expects($this->at(0))->method('getTemperature'); $mock->expects($this->at(2))->method('showWord'); $mock->expects($this->at(1))->method('getWord'); $mock->process(); }
      
      





PHPUnitで呼び出しの期待値をテストするための1回の()およびat()に加えて、any()、never()、atLeastOnce()およびちょうど($ count)の構造もあります。 彼らの名前は彼ら自身のために語っています。



結果のオーバーライドを返す



はるかに、Mockオブジェクトの最も便利な機能は、スプーフィングされたメソッドによって返された結果をエミュレートする機能です。 クラスのprocess()メソッドに戻ります。 温度センサーへのアピールがあります-getTemperature()。 しかし、実際にはセンサーがないことも覚えています。 たとえそれがあったとしても、考えられるすべての状況をテストするために、15度以下に冷却したり、25度以上に熱したりすることはありません。 ご想像のとおり、この場合、モックオブジェクトが役立ちます。 will()を使用して、目的のメソッドに必要な結果を返すようにできます。 以下に例を示します。

 /** * @dataProvider provider_process */ public function test_process($temperature) { $mock = $this->getMock('MyClass', array('getTemperature', 'getWord', 'showWord')); //  getTemperature()   $temperature $mock->expects($this->once())->method('getTemperature')->will($this->returnValue($temperature)); $mock->process(); } public static function provider_process() { return array( 'cold' => array(10), 'warm' => array(20), 'hot' => array(30), ); }
      
      





明らかに、このテストは、テストクラスが処理できるすべての可能な値を対象としています。 PHPUnitは、will()で使用する次の構成要素を提供します。



引数を検証する



テスト用のMockオブジェクトのもう1つの便利な機能は、with()構造を使用してスプーフィングされたメソッドを呼び出すときに指定された引数をチェックすることです。

 public function test_with_and_will_usage() { $mock = $this->getMock('MyClass', array('getWord')); $mock->expects($this->once()) ->method('getWord') ->with($this->greaterThan(25)) ->will($this->returnValue('hot')); $this->assertEquals('hot', $mock->getWord(30)); }
      
      





()の引数は、assertThat()チェックとすべて同じ構造を取ることができるため、ここでは詳細な説明なしに可能な構造のリストのみを提供します。



これらの構造はすべて、論理構造logicalAnd()、logicalOr()、logicalNot()、logicalXor()を使用して組み合わせることができます。

 $mock->expects($this->once()) ->method('getWord') ->with($this->logicalAnd($this->greaterThanOrEqual(15), $this->lessThanOrEqual(25))) ->will($this->returnValue('warm'));
      
      





PHPUnitのMockオブジェクトの機能に完全に慣れたので、クラスの最終テストを実行できます。

 /** * @dataProvider provider_process */ public function test_process($temperature, $expected_word) { //  Mock ,  getWord()  process()      $mock = $this->getMock('MyClass', array('getTemperature', 'showWord')); //  getTemperature()    $temperature $mock->expects($this->once())->method('getTemperature')->will($this->returnValue($temperature)); // ,   showWord()    $expected_word $mock->expects($this->once())->method('showWord')->with($this->equalTo($expected_word)); //  $mock->process(); } public static function provider_process() { return array( 'cold' => array(10, 'cold'), 'warm' => array(20, 'warm'), 'hot' => array(30, 'hot'), ); }
      
      





UPD: VolCh 、上記のようなテストの作成はアンチパターンであると正しく述べました。 したがって、上記の例は、PHPUnitのMockオブジェクトの機能に慣れるためにのみ考慮されるべきです。



静的メソッドの置換



PHPUnitバージョン3.5以降では、静的構築staticExpects()を使用して静的メソッドを置き換えることができます。

 $class = $this->getMockClass('SomeClass'); //    PHP  5.3   //       call_user_func_array() $class::staticExpects($this->once())->method('someStaticMethod'); $class::someStaticMethod();
      
      





この技術革新を実際に適用するには、テスト対象クラス内で、交換可能な静的メソッドの呼び出しが次のいずれかの方法で発生する必要があります。



self制限により、この方法でクラス内で呼び出される静的メソッドの置換は機能しません。

 self::staticMethod();
      
      





おわりに



結論として、テストされたクラスを外部データソースから隔離する以外の目的で、Mockオブジェクトに夢中になりすぎないようにする必要があります。 それ以外の場合、ソースコードにわずかな変更を加えても、テスト自体も同様に編集する必要があります。 かなり重要なリファクタリングは、完全に書き直さなければならないという事実につながる可能性があります。



All Articles