48JIGEN *Reloaded*

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を使うとか、フィクスチャを作成する方法は探すと沢山出てくるけど、じゃあ別のシーンで例えば

そんな別のシチュエーションに直面したときに、また別のフィクスチャを作成するツールや手段を探すのはしんどい。

特定の目的に特化していない、汎用的なモックフレームワークを覚えておくと、こういう時に役に立つ。どんな状態のソースコード環境においても設計を検証したりリファクタリングを進めたりしやすくなる。

次の章では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には無い機能を多く揃えてる。

個人的な感想をまとめると、本格的にモックフレームワークを使い込みたいならMockeryを使うと良い。逆に、まずは少し使ってみたいだけならPHPUnitで十分かなと思った。

モックを使ったテスト駆動開発のとっかかりとして、PHPの定番テストハーネスであるPHPUnitを使ったモックの書き方を覚えておくのは、悪いアイデアじゃない。

参考:あわせて読みたい

挨拶代わりに写経する

このブログをここまで読んでいる人は大体恐らく、この秋に発売されて話題になっている実践テスト駆動開発について聞いたことのある人だと思うのだけど、訳者の和智さんがブログで紹介されているMock Roles, Not Objects(邦題:ロールをモックせよ)が、モックフレームワークを使ってみる題材として適当なので紹介しておきたい。

上記ブログでダウンロードできるpdf、全編読みやすい日本語でたった30pしかないのに、内容が濃い。ゆっくり読むことをオススメする。なお、pdfの最後のページに角谷さんと高木さんへの謝辞が静かに記載されているところを見て、実践テスト駆動開発が訳されるに至る文脈を推し量ったりできるのも個人的にはポイントが高い。

リファレンスにできる写経結果はたくさんある

PHPUnitとMockeryで書いたコードのサンプルを僕のgithubアカウントに上げてある。間違ってるところがあるかもしれないので、訂正を歓迎します。他にも@iakioさんも写経されてるようだし、Javaなら訳者和智さんのgithubのコードが参考になる。他にもネットを探せば写経結果をいくつも見つけることができる。(Javaのサンプルは本当に豊富で、jmock1を使った実装テスト、jmock2を使ったサンプルなど)

写経する上で抑えておくべきポイントとか

僕の体験談を書いておく。@iakioさんのブログにも写経の感想が書いてあって、こちらも参考になると思う。

        $hoge = $this->getMock('PDO', array('query'))
            ->expects($this->any())
            ->method('query')
            ->will($this->returnValue(array('id'=>7, 'name'=>'Tarou Yamada')));

とまあ、こんな感じで、テスト書くのもモック使うのも楽しいし色々捗るので、PHPがまた少し好きになった。

*1原著ではHashMapクラスのインスタンスとして$cachedvaluesを生成してるようなので、本当はPHP版のHashMapクラスを厳密に定義して、そのインスタンスを生成すべきなのですが、話を簡潔にするために、ここではそのステップを省略します

このエントリーをはてなブックマークに追加

Related Posts

4つのルールと5つのコツでチラ見するテスト駆動開発入門 ~本を読んでTDDを実践したまとめ

#php_tdd_ci に参加して知った仮実装の付加価値と、例外処理のプラクティス

#phpmatsuri in Sapporo コードと雑感

 

about me

@remore is a software engineer, weekend contrabassist, and occasional public speaker. Read more