PHPUnitのモックで設計とリファクタが捗る
2012/10/31
例えば設計の最中に次のようなクラスを仮実装したとして
class TwitterClient { public function tweet($message) { return true; } } $hoge = new TwitterClient(); $hoge->tweet('大心なう'); // trueが返る
PHPUnitを使うと、モックを使ってこんな風に同じ仮実装を書ける。
class ClientContainerTest extends PHPUnit_Framework_TestCase { public function test_モックを書いてみる() { $hoge = $this->getMock('TwitterClient', array('tweet')); $hoge->expects($this->any()) ->method('tweet') ->will($this->returnValue(true)); $hoge->tweet('大心なう'); // trueが返る } }
method()、will()、returnValue()あたりは何やってるかなんとなく空気を読める。expects()では、呼び出し回数の制限を設定してる。any()で指定しておくと、何回でも呼ぶ事ができるメソッドを定義してることになる。例えば一回しか呼んではいけないメソッドを定義するときは、once()を指定する。
これだけだとモックの使いどころがイメージしづらいので、次にPDO経由でDBを操作するシーンを考える。
例えば、諸般の事情でDBサーバが動いてない環境でテストを実行したいとする。PHPの素のコードを例えばこんな風に書いてみる。
class PDO { public function __construct($dsn) { return true; } public function query($sql) { return new PDOStatement(); } } $pdo = new PDO('sqlite:memory'); // ここでエラー $pdo->query('SELECT * FROM users;');
当たり前だけど、このコードを実行すると"Cannot redeclare class PDO"とFatalが出る。PDOはPHP組み込みで提供されてるクラスだから、同名のクラスを重複して宣言できるわけがない。オンメモリでDBを構築できるsqlite:memoryをDSNに使うっていうアイデアは良かったんだけど。
こんな時にモックを使うことができる。
class PDOContainerTest extends PHPUnit_Framework_TestCase { public function test_queryをスタブ化する() { $hoge = $this->getMock('PDO', array('query'), array('sqlite:memory')); $hoge->expects($this->any()) ->method('query') ->will($this->returnValue(new PDOStatement())); $hoge->query('SELECT * FROM users;') // new PDOStatement()の結果が返る $hoge->getAvailableDrivers(); // スタブ化してないメソッドも利用可能 } }
これを実行すると、スタブ化したquery()メソッドだけではなく、PDOクラスに元々実装されてるメソッドも得ることができる。このように、既存のクラスのうち一部のメソッドをモック化することをパーシャルモック(Partial Mock: 部分的なモック)と呼ぶ。
この例のようにDBをスタブ化したいだけの目的だったらPHPUnitのDatabase Extensionを使うとかPhactoryを使うとか、フィクスチャを作成する方法は探すと沢山出てくるけど、じゃあ別のシーンで例えば
- APIを叩いてて外部モジュールの振る舞いに依存してるだとか
- memcachedが保持してる揮発性のデータに依存してるだとか
そんな別のシチュエーションに直面したときに、また別のフィクスチャを作成するツールや手段を探すのはしんどい。
特定の目的に特化していない、汎用的なモックフレームワークを覚えておくと、こういう時に役に立つ。どんな状態のソースコード環境においても設計を検証したりリファクタリングを進めたりしやすくなる。
次の章ではPHPにはどういう機能や特性を持ったモックフレームワークがあるのかについて簡単に調べた内容をまとめてみる。目的にかなったモックフレームワーク選びがしやすくなるように情報を拾って読んでもらいたい。
PHPのモックフレームワーク、思ってたより色々あった
調べてみた。2012年10月時点の調査。
フレームワーク | 必要なPHP環境 | 本家 | 備考 |
---|---|---|---|
PHPUnit | PHPUnit3.7はPHP 5.3.3以上で動作(PHP 5.4.7以上を推奨) PHPUnit3.6ならPHP5.2でも動作する。 |
本家 | サンプルコード |
Mockery | PHP5.3.2以上 | 本家 | サンプルコード |
Phake | PHP5.2.4以上 | 本家 | サンプルコード |
SimpleTest | SimpleTest1.1はPHP5.0.3以上で動作。 それ以前のバージョンはPHP4系でも動作。 |
本家 | サンプルコード |
Phockito | 未確認 | 本家 | ちゃんと調べてない |
yayMock | 未確認 | 本家 | ちゃんと調べてない |
ここではPHPUnitとMockeryをピックアップして、独断と偏見で機能の性格や使いやすさを評価してみる。
(ちなみに、Phakeはphp5.2系で使えるのに機能が豊富でこれも良さそうだった。詳しく見たい人は、@hidenorigotoさん執筆の「Phake,Mockeryによるオブジェクト指向プログラミング」(WEB+DB PRESS vol.70)を参照すると良さそう。)
機能 | PHPUnit 3.7 |
Mockery 0.7.2 |
備考 |
---|---|---|---|
メソッドの呼出回数制限 | ○ |
◎ |
Mockeryの方が最大(atMost)・最小(atLeast)などの制限の指定ができてより柔軟 |
パーシャルモック | ○ |
◎ |
Mockeryの方が宣言を簡潔に書ける |
引数のマッチング | ○ |
○ |
PHPUnitのwith()による引数マッチングには、このページの表4.3の制約の表全てが使える。Mockeryについては、README.mdのここを参照。 |
モックのカスケーディング (メソッドチェーンを使ったテスト) |
○ |
◎ |
PHPUnitだと一応できなくはないが強引で好きじゃない(同様の例)。MockeryはREADME.mdにサンプルがある。 |
メソッドの呼び出し順序のテスト | × |
○ |
Mockeryでは可能だが、使いやすくはない |
上記の表を見てわかる通り、機能的にはMockeryの方がよく整備されている印象を受けた。そのほかにも、PHPUnitには無い機能を多く揃えてる。
- Publicプロパティのモックだとか
- 変数引数の挙動の再現だとか
- (クロージャをモックの引数に取れるため)モックを使ったテスト対象クラスのメソッドの使い方のレコーディングとテスト(リファクタリングの際に便利)だとか。
個人的な感想をまとめると、本格的にモックフレームワークを使い込みたいならMockeryを使うと良い。逆に、まずは少し使ってみたいだけならPHPUnitで十分かなと思った。
モックを使ったテスト駆動開発のとっかかりとして、PHPの定番テストハーネスであるPHPUnitを使ったモックの書き方を覚えておくのは、悪いアイデアじゃない。
参考:あわせて読みたい
- PHPUnitとMockeryの使用感の違いを解説してる記事。英語 / Why Mockery?
- Mockery, Phake, Phockito, PHPUnitでのモックの書き方を比較したコード例 / gist: 1753804 / Example temperature webservice
- PHPのモッキングフレームワーク "Phake" を使ってみる | アライドアーキテクツ エンジニアブログ
- Phake使ってみる:準備変
挨拶代わりに写経する
このブログをここまで読んでいる人は大体恐らく、この秋に発売されて話題になっている実践テスト駆動開発について聞いたことのある人だと思うのだけど、訳者の和智さんがブログで紹介されているMock Roles, Not Objects(邦題:ロールをモックせよ)が、モックフレームワークを使ってみる題材として適当なので紹介しておきたい。
上記ブログでダウンロードできるpdf、全編読みやすい日本語でたった30pしかないのに、内容が濃い。ゆっくり読むことをオススメする。なお、pdfの最後のページに角谷さんと高木さんへの謝辞が静かに記載されているところを見て、実践テスト駆動開発が訳されるに至る文脈を推し量ったりできるのも個人的にはポイントが高い。
リファレンスにできる写経結果はたくさんある
PHPUnitとMockeryで書いたコードのサンプルを僕のgithubアカウントに上げてある。間違ってるところがあるかもしれないので、訂正を歓迎します。他にも@iakioさんも写経されてるようだし、Javaなら訳者和智さんのgithubのコードが参考になる。他にもネットを探せば写経結果をいくつも見つけることができる。(Javaのサンプルは本当に豊富で、jmock1を使った実装とテスト、jmock2を使ったサンプルなど)
写経する上で抑えておくべきポイントとか
僕の体験談を書いておく。@iakioさんのブログにも写経の感想が書いてあって、こちらも参考になると思う。
- まず前提として、OOPやデザインパターンを少しでもかじっておいた方が楽しめる。"継承ではなく委譲に基づいたオブジェクト指向スタイル"だとか、"コンポジションによるプログラミング"だとか、OOPを念頭において書かれている。
- 環境のセットアップにはsuinさんのQiita記事がわかりやすくてよかった。composerが使えないphpが5.3未満のバージョンの人は、PEARでインストールが必要。
- ちなみに、PHPUnit3.7.5からはphar(PHP Archive)でも使える
- 細かいことだけど、PHPUnitだとgetMock()でモックを生成するだけで、PHPUnit実行結果のassertionsの数がインクリメントされてしまう動作をするので注意。テスト終了時にverify()相当の機能が自動で走るためだと思うんだけど、testを書いてないのにassertの数が増える動作をして若干驚いたのでメモ。
- getMock()の結果を使ってメソッドチェーンは作れない。stackoverflowで同じことやってる人がいたのでわかったのだけど、こんなコードは書けない。expects()以降のチェーンは分けて書かないといけない。
$hoge = $this->getMock('PDO', array('query')) ->expects($this->any()) ->method('query') ->will($this->returnValue(array('id'=>7, 'name'=>'Tarou Yamada')));
- 10ページ目、HashMapって単語が出てきて何のことかと思った。Javaやってる人にはすぐわかるんでしょうね。一旦PHPで言うところのArrayで置き換えましょう。(ArrayでHashMapクラス相当の機能が完全に置き換えられるかどうかはまた別の話。突っ込んで考えたい人はstackoverflowにスレが立ってたのでそちらを)*1
- 12ページ目などで、"verify()のときに検出され"みたいな説明が頻出するけど、PHPUnitではverify()メソッドの呼び出しなしでテスト終了時に自動的にverify()されるので、このくだりは無視して問題ない。一方で、Mockeryの場合はmockery_verify(phakeならverify())がこれに相当する。
- 12ページ目でインナークラスが出てくるけど、phpではまだサポートされていないので、一旦クラスの外に置けばOK
とまあ、こんな感じで、テスト書くのもモック使うのも楽しいし色々捗るので、PHPがまた少し好きになった。
*1原著ではHashMapクラスのインスタンスとして$cachedvaluesを生成してるようなので、本当はPHP版のHashMapクラスを厳密に定義して、そのインスタンスを生成すべきなのですが、話を簡潔にするために、ここではそのステップを省略します
Related Posts
4つのルールと5つのコツでチラ見するテスト駆動開発入門 ~本を読んでTDDを実践したまとめ
about me
@remore is a software engineer, weekend contrabassist, and occasional public speaker. Read more