ごあいさつ
はじめまして。アライドアーキテクツの石川と申します。
2月に入社してから早くも3ヶ月がたとうとしています。
私は前の職場ではほとんどjavaで開発していたのですが、
アライドに入社してからは主にPHPで開発をしています。
私の回では、その時その時に関心のある技術情報を発信していければ、と思っています。
皆様どうぞよろしくお願いいたします。
今の自分の課題はとにかくUnitテストを書くことなので、
今回はPHPのモッキングフレームワークである「Mockery」について書きたいと思います。
Mockeryって?
padraic / mockery
こちらで御座います。
static methodのモック化が出来たり、テスト対象のメソッド内部で生成されるインスタンスをモック化したり、
PHPUnitでやろうとすると一手間かかることをサクっと出来てしまいます。
Let’s インストール
Composerやgit cloneを使う方法もありますが、今回はPEARでいきます。
1 2 3 |
sudo pear channel-discover pear.survivethedeepend.com sudo pear channel-discover hamcrest.googlecode.com/svn/pear sudo pear install --alldeps deepend/Mockery |
セットアップ
まずはMockeryのパスを通します。
その後、以下のコードをbootstrapで指定したファイルに書き、Mockeryを使用可能な状態にします。
1 2 3 4 |
require_once 'Mockery/Loader.php'; require_once 'Hamcrest/Hamcrest.php'; $loader = new \Mockery\Loader; $loader->register(); |
では早速使ってみましょう
以下の様なクラスがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php class SampleExecuter { private $checker; public function __construct($checker) { $this->checker = $checker; } public function doSomething($param) { if ($this->checker->checkSomething($param)) { return 'trueが返ったよ'; } else { return 'falseが返ったよ'; } } } |
通常これをテストする場合、先にcheckSomethingメソッドを実装したチェッカークラスが必要です。
しかし、Mockeryを使えばチェッカークラスの実装をせずにサクッとテストコードが書けちゃいます。
下の様な感じですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php use \Mockery as M; class SampleTest extends PHPUnit_Framework_TestCase { public function test_doSomeThing_チェックOK() { $checkerMock = M::mock('XXXChecker'); // 'XXXChecker'という名前を持つモックを作成 $checkerMock ->shouldReceive('checkSomething') // checkSomethingというメソッドが呼び出される ->with('foo') // 引数として'foo'を受け取る ->andReturn(true); // trueを返却する $target = new SampleExecuter($checkerMock); $this->assertEquals('trueが返ったよ', $target->doSomething('foo')); // OK!! } public function test_doSomeThing_チェックNG() { $checkerMock = M::mock('XXXChecker'); $checkerMock ->shouldReceive('checkSomething') ->with('foo') ->andReturn(false); // falseを返却する $target = new SampleExecuter($checkerMock); $this->assertEquals('falseが返ったよ', $target->doSomething('foo')); // OK!! } } |
チェッカークラスの実装無しにSampleExecuterクラスのテストが書けました。
これでチェッカークラスの実装にもたついているT君を横目に自分は華麗にテストできますね!
では、下の様なケースではどうでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php class SampleExecuter { public function doSomething($checkType, $param) { $factory = new CheckerFactory($checkType); $checker = $factory->create(); if ($checker->checkSomething($param)) { return 'trueが返ったよ'; } else { return 'falseが返ったよ'; } } } |
1 |
new CheckerFactory($checkType) |
CheckerFactoryがdoSomethingメソッド内でnewされています。
こんな感じのコードは実際の現場では良く見かけます。
がしかし、単体テストを書く際には非常に頭を悩ますコードではないでしょうか。
これが単なるFactoryでは無くてDB接続しに行く様なクラスだった場合はその時点でハイ投了、
テストを書かずに眠れぬ夜を過ごすわけですね。これは困りました。
・・・えっ、単体テストできる?
「Mockery」を使えば、ですって・・・?
モノは試しです。サクッとテストを書いてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php use \Mockery as M; class SampleTest extends PHPUnit_Framework_TestCase { public function test_doSomeThing_チェックOK() { $target = new SampleExecuter(); $checkerMock = M::mock('XXXChecker'); $checkerMock ->shouldReceive('checkSomething') ->with('foo') ->andReturn(true); $factoryMock = M::mock('overload:CheckerFactory'); // 'overload:'という接頭語をつけてモック化する $factoryMock->shouldReceive('create')->andReturn($checkerMock); $this->assertEquals('trueが返ったよ', $target->doSomething('hoge','foo')); // お、OK!! } } |
なんということでしょう。
doSomethingメソッド内部で生成されるCheckerFactoryをモック化できてしまいました。
これで安心して夜も眠れます。明日も生き生きプログラミングできます。
では、下の様なケースではどうでしょうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php class SampleExecuter { public function doSomething($checkType, $param) { $factory = CheckerFactory::getInstance(); $checker = $factory->create(); if ($checker->checkSomething($param)) { return 'trueが返ったよ'; } else { return 'falseが返ったよ'; } } } |
1 |
CheckerFactory::getInstance() |
で、出たー「::」。staticの大将が満を持して登場です。
これはもう終わった。涙で前が見えないリグレッションテスト地獄に突入です。
どこまでも深い闇に落ちていく感覚を貴方は覚えるでしょう。
ご安心ください!
「Mockery」はそっと救いの手を差し伸べます。
そう、そこの貴方ももうお気づきですね。
Mockeryならstaticもサクッとテストできてしまうんです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php use \Mockery as M; class SampleTest extends PHPUnit_Framework_TestCase { public function test_doSomeThing_チェックOK() { $target = new SampleExecuter(); $checkerMock = M::mock('XXXChecker'); $checkerMock ->shouldReceive('checkSomething') ->with('foo') ->andReturn(true); $factoryMock = M::mock('XXXFactory'); $factoryMock->shouldReceive('create')->andReturn($checkerMock); $SingletonMock = M::mock('alias:CheckerFactory'); // 'alias:'という接頭語をつけてモック化する $SingletonMock->shouldReceive('getInstance')->andReturn($factoryMock); $this->assertEquals('trueが返ったよ', $target->doSomething('hoge','foo')); // これもOK!!やったー!! } } |
Excellent!!
これであなたもリグレッションテスト地獄から抜け出して安らかな眠りにつくことうけあいです。
「リファクタリングしたい・・・。でも稼働しているシステムを壊すの怖いよ・・・」
「Mockery」は貴方のそんな想いにきっと応えてくれることでしょう。
注意点
「overload:」と「alias:」を使う時の注意点として。
(当然といえば当然なのですが)対象のクラス(上記例でのCheckerFactory)がロード済になっている場合はこれを使用できません。
実際のクラスがロードされる前にモック化したクラスを定義して、そいつをロードしているわけですね。
なので下の様なテストコードは通りません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function test_doSomeThing_チェックOK1() { ~~~ 省略 ~~~ $SingletonMock = M::mock('alias:CheckerFactory'); $SingletonMock->shouldReceive('getInstance')->andReturn($factoryMock); $this->assertEquals('trueが返ったよ', $target->doSomething('hoge','foo')); } public function test_doSomeThing_チェックOK2() { ~~~ 省略 ~~~ $SingletonMock = M::mock('alias:CheckerFactory'); // ←Cannot redeclare class CheckerFactory $SingletonMock->shouldReceive('getInstance')->andReturn($factoryMock); $this->assertEquals('trueが返ったよ', $target->doSomething('hoge','foo')); } |
同一プロセスでは同じ名前のクラスを定義できないので、怒られてしまうんですね。
これの対処法としては、
1.phpunitのオプションにてprocess-isolationをON
2.対象メソッドに「@runInSeparateProcess」アノテーションを付与する
なんかにしとくといいと思います。パフォーマンスコストは上がっちゃいますが。
※ちなみにmakegoodではStagehand_TestRunnerがprocessIsolationに対応していない為
エラーとなってしまうようです。
参考:makegood を試す
おわりに
長々と書いてきましたが、Mockeryは総じて使いやすいフレームワークかなと思います。
他にもパーシャルモックやメソッド呼び出し順序の指定などの機能も持っているので、
気になった方は本家のドキュメントを読んでみて下さい。
最後にアライドアーキテクツでは、一緒に単体テストしてくれるエンジニアを募集中です。
興味がある方は、こちらの採用サイトからご応募ください。
アライドアーキテクツでVPoPをしています。おもにダイエットに関する話を書きます。たまにサービス開発において大事だと思っていることを書いたりします。