Mockeryでファンクションをモックする

タグ: Mockery   テスト志向   Laravel4  

ユニットテストのお話です。グローバルのファンクションを使用すると、テストがしづらくなると言われています。置き換えができないからです。

追記:内容はやや古くなりました。Mockery1.0.0のドキュメントを翻訳しましたので、そちらを主に参考にしてください。

https://readouble.com/mockery/1.0/ja/index.html

  1. <?php
  2.  
  3. class Sample
  4. {
  5. function echoFile( $filename )
  6. {
  7. echo file_get_contents( $filename );
  8. }
  9. }

これがテストしづらいのは、このままではダミーのファイルを作成し、それを読み込んで表示した内容を目視で比較しなくては、動作確認ができないからです。(まあ、出力結果をファイルにリダイレクトすることもできますが、手間なのには変わりありません。)

そこでLaravelを始めとするフレームワークでは、よく使用するこの手の関数をクラスにまとめています。この場合のfile_get_contentsは、Laravelの場合、Fileクラスに含まれています。

  1. function echoFile( $filename )
  2. {
  3. echo File::get( $filename );
  4. }

これでPHPUnitからテストが簡単に行われます。Laravelの場合、Fileクラスはファサードで、モックへの置き換えが特に簡単です。

  1. function testEchoFile()
  2. {
  3. File::shouldReceive('get')->andReturn('お好きな内容');
  4. ...
  5. }

呼び出しているFileクラスのメソッドが一回なら、これで自動的に置き換えられます。(複数の場合はMockeryのモックオブジェクトを取得し、それに条件を付け加えます。)

どうしてFileクラスが存在しているのか、テストが必要がない人は理解が難しい部分です。全く、関数をそのまま呼び出しているものが多いからです。理由はテストが簡単になるからです。

さて、全部の関数がLaravelへ用意されたクラスに取り込まれているわけではありません。

  1. <?php
  2.  
  3. class Sample
  4. {
  5. function echoTime( )
  6. {
  7. echo time( );
  8. }
  9. }

これを取り扱うためのテストライブラリーもありますが。ほんのちょっとの工夫でMockeryで処理可能です。

好ましいのはクラスを作成し、そこにまとめることですが、これは管理が煩雑に生る欠点もあります。そこで、グローバル関数の呼び出しは、そのままメソッドに分けましょう。

  1. class Sample
  2. {
  3. function echoTime( )
  4. {
  5. echo $this->time( );
  6. }
  7.  
  8. function time() {
  9. return time()
  10. }
  11. }

通常、ユニットテストする場合はSampleをそのままnewで生成しますが、このような場合はパーシャルモックにします。そのパーシャルモックを通してテストします。

  1. function testEchoDate()
  2. {
  3. $sample = Mockery::mock('Sample')->makePartial();
  4. $sample->shouldReceive('time')->andReturn(お好きな数字);
  5. ...
  6. }

->makePartial()でパーシャルモックにすると、呼び出しが定義されて場合のみモックされます。その他の呼び出しは、もとのクラスのオブジェクトを呼び出してくれます。つまり、この形式のモックは、モックオブジェクトの中に本当のオブジェクトを持っており、モックしたい部分の条件を書いておけば、当てはまるメソッドの呼び出しだけをモックし、それ以外は本当のオブジェクトのメソッドを呼び出してくれます。

名前空間付きで、コンストラクターへの引数が必要な場合の指定は、以下のサンプルから読み取って下さい。

  1. // テスト対象
  2. namespace Root\Sub;
  3.  
  4. class Sample
  5. {
  6.  
  7. public function __construct( $arg1, $arg2 )
  8. {
  9. $this->arg1 = arg1;
  10. $this->arg2 = arg2;
  11. }
  12.  
  13. public function doSomething()
  14. {
  15. return $this->time();
  16. }
  17.  
  18. public function time()
  19. {
  20. return time();
  21. }
  22.  
  23. }
  24.  
  25. // テストクラス
  26. use Mockery as M;
  27.  
  28. class SampleTest
  29. {
  30.  
  31. public function testDoSometing()
  32. {
  33. $testArg1 = 'コンストラクター引数1';
  34. $testArg2 = 'コンストラクター引数2';
  35.  
  36. $expected = 200000;
  37.  
  38. $sample = M::mock( 'Root\Sub\Sample', array( $testArg1, $testArg2 ) )
  39. ->makePartial();
  40. $sample->shouldReceive( 'time' )
  41. ->andReturn( $expected );
  42.  
  43. $this->assertEquals( $expected, $sample->doSomething() );
  44. }
  45.  
  46. }

Mockeryのパーシャルモック、わかってしまえば、使えますよ。