Practical Symfony #4: SymfonyのテストコードをPhakeで書き換えてみる : Symfony Advent Calender 2011 JP - 16日目
PHPメンターズの後藤です。クリスマスまでの24日間をSymfonyの記事で楽しもう!というSymfony Advent Calendar JP 2011の16日目の記事です。前回はSymfony2のテストコードを読むにてSymfonyのHttpKernelコンポーネントにあるKernelクラスのテストコードを紹介し、PHPUnitのモックオブジェクトを使っている例を説明しました。
今回は、前回紹介したテストコードを、PHPのモッキングフレームワークであるPhakeを使って書き換えてみましょう。
SymfonyのテストでPhakeを使うための準備については、こちらの記事を参照してください。
まず、前回取り上げたテストコードを最初に再掲します。
tests/Symfony/Tests/Component/HttpKernel/KernelTest.php:
class KernelTest extends \PHPUnit_Framework_TestCase { // snip public function testBootInitializesBundlesAndContainer() { $kernel = $this->getMockBuilder('Symfony\Tests\Component\HttpKernel\KernelForTest') ->disableOriginalConstructor() ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles')) ->getMock(); $kernel->expects($this->once()) ->method('initializeBundles'); $kernel->expects($this->once()) ->method('initializeContainer'); $kernel->expects($this->once()) ->method('getBundles') ->will($this->returnValue(array())); $kernel->boot(); }
このコードにおけるテストの意図を、Phakeを使って順に再現します。まず最初は、テスト対象であるboot()メソッドの呼び出しを記述します。
class KernelTest extends \PHPUnit_Framework_TestCase { // snip public function testBootInitializesBundlesAndContainer() { $kernel->boot(); }
$kernelを用意しなければなりません。このテストでは、同じクラスの別メソッドを呼び出すという振舞をテストしたいため、Kernelの実オブジェクトではなく、純粋なモックオブジェクトでもなく、パーシャルモックオブジェクトを利用します。
※PHPUnitのテストコードで作成しているのも、同じようにパーシャルモックオブジェクトです。
class KernelTest extends \PHPUnit_Framework_TestCase { // snip public function testBootInitializesBundlesAndContainer() { $kernel = \Phake::partialMock('Symfony\Tests\Component\HttpKernel\KernelForTest', 'env', true); $kernel->boot(); }
基本的な準備はこれで整いましたが、これでは何もテストしていません。元のテストコードでテストしていたboot()メソッドの振る舞いである、initializeBundles(), initializeContainer(), getBundles()メソッドが呼び出されていることを検証するテストコードを追加しましょう。このようなメソッド呼び出しの検証には、Phake::verifyを使います。
class KernelTest extends \PHPUnit_Framework_TestCase { // snip public function testBootInitializesBundlesAndContainer() { $kernel = \Phake::partialMock('Symfony\Tests\Component\HttpKernel\KernelForTest', 'env', true); $kernel->boot(); \Phake::verify($kernel)->initializeBundles(); \Phake::verify($kernel)->initializeContainer(); \Phake::verify($kernel)->getBundles(); }
このようにboot()メソッドを実行した後、実際に各メソッドが呼び出されたことを検証(verify)するように記述します(ここではそれぞれ1回ずつ呼び出されたことを検証しています)。
これでよさそうにも見えますが、このテストではinitializeBundles(), initializeContainer(), getBundles()それぞれの実装には触れたくありません。ですので、これらの3つのメソッドをモックで置き換えます。メソッドをモックで置き換えるには、Phake::whenを使います。
class KernelTest extends \PHPUnit_Framework_TestCase { // snip public function testBootInitializesBundlesAndContainer() { $kernel = \Phake::partialMock('Symfony\Tests\Component\HttpKernel\KernelForTest', 'env', true); \Phake::when($kernel)->initializeBundles()->thenReturn(null); \Phake::when($kernel)->initializeContainer()->thenReturn(null); \Phake::when($kernel)->getBundles()->thenReturn(array()); $kernel->boot(); \Phake::verify($kernel)->initializeBundles(); \Phake::verify($kernel)->initializeContainer(); \Phake::verify($kernel)->getBundles(); }
これで完成です。最初のコードと見比べてみてください。このテストメソッドでテストしている内容と意図を、コードから明確に読み取れるようになっていることが分かります。
まとめ
モックオブジェクトを使うことで、テストしたい対象だけに焦点を絞ることができるようになりますが、Phakeのように、よりしなやかに開発者の意図を記述できるフレームワークを利用することで、読みやすくメンテナンス性の高いテストコードを書くことができます。
このようにしてテストコードに開発者の意図が反映しやすくなることは、ドメインモデルの改善やブレイクスルーにもつながります。