PhakeのモックでCakePHPをテスト コントローラ篇・改
こんにちは。前回のPhake のモックで CakePHP をテスト コントローラ篇にもレスポンスが頂けて嬉しいです。天の声に耳を傾けていると(ただのエゴ・サーチです…)素敵なご指摘が聞けたので、改訂版として掲載します。予定していた「コンポーネント篇」は改訂版のおかげで不要になりました! 準備も少なくて済む画期的な方法です。
Controller で Phake モックを使いたいという背景については前回の記事をそのまま残しますのでご参照ください。
--
※本記事では CakePHP に Phake を導入する手法について解説しています。Phake 自体の使い方や PHPUnit 標準モックとの比較は過去の記事にて紹介しています。
2 段階のサブクラスを作成する
前回と同じく、CakePHP のControllerTestCase
から機能を拡張するためMyControllerTestCase
を作成します。abstract class
として継承しないとテスト実行時に「テストがありません」と怒られます。
abstract class MyControllerTestCase extends ControllerTestCase
{
public function setUp()
{
parent::setUp();
}
// 略
}
今回は例としてPostsController
という Controller についてテストを行うため、さらにもう 1 段階サブクラスを作成します。
abstract class PostsControllerTestCase extends MyControllerTestCase
{
public function setUp()
{
parent::setUp();
}
// 略
}
PostsController
にはindex()
やadd()
やedit()
といったメソッドが備わっているとします。
これらのテストを 1 つのファイルで扱う「1 クラス-1 テストクラス」でも問題はありませんが、モック・テストは行数が嵩むため、分割して「1 メソッド-1 テストクラス」で進める方がクラスごとに共通処理をくくるsetUp()
の恩恵を受けやすく、効率が良いと感じています。
PostsControllerIndexTest
PostsControllerAddTest
PostsControllerEditTest
例としてこのようなテストクラスを作成します。これらは全てPostsControllerTestCase
を継承します。
Phake モック注入のための準備
ここからが前回と大きく異る方法です。天の声によるとClassRegistry
という CakePHP API を活用すれば良いと聞こえました。恥ずかしながら私はこのクラスの存在を知らなかったのですが、調べてみると非常によくできたクラスで、「CakePHP は高結合度で使いにくいヤツだ!」と dis っていたことはこの場で謝ります。
前回はFactoryComponent
を作成し
- PHPUnit モック生成 →
FactoryComponent
スタブ定義 →Phake モック注入
という方法をとっていました。今回、改訂版では PHPUnit モックすら使わずいきなり
-
ClassRegistry
→Phake モック注入
とできます。準備の行数や手間も遥かに少なくて済みます。
ClassRegistry を用いた injection()を実装する
abstract class MyControllerTestCase extends ControllerTestCase
{
public function setUp()
{
parent::setUp();
}
public function tearDown()
{
parent::tearDown();
}
public function convertToKey($mock)
{
$class = get_class($mock);
$array = explode('_', $class);
$result = preg_replace('/(Helper|Component)/', '', $array[0]);
return $result;
}
public function injection($mock)
{
$args = func_get_args();
foreach ($args as $k => $v) {
$key = $this->convertToKey($v);
ClassRegistry::addObject($key, $v);
}
}
}
MyControllerTestCase
にinjection()
を実装します。convertToKey()
メソッドはクラス名文字列を取得するためのものです。Phake ではModel_PHAKE53205b373bad5
というような固有の ID をクラス名に与えるようで、この先頭部分のみ取り出すためです。さらに CakePHP の規則では Controller 内ではFooComponent
やBarHelper
といったクラス名の先頭部分(Foo, Bar)しかプロパティ名として扱わないため、適合するように変換しています。
injection()
は前回と異なり、ClassRegistry::addObject()
を用いています。可変引数として、渡したモックを全て注入します。
各 Controller ごとの TestCase を準備
MyControllerTestCase
のサブクラスでも準備が必要です。PostsControllerTestCase
に次のように記述します。
abstract class PostsControllerTestCase extends MyControllerTestCase
{
public function setUp()
{
parent::setUp();
$this->controller = $this->generate('Posts');
}
public function tearDown()
{
parent::tearDown();
}
}
$this->generate()
を使う点は前回と同じですが、ここで Component 群のモックを作成していません。Controller のモックを作成するのみです。
ここも省略してしまうとClassRegistry
の内容が断絶してしまったので、PHPUnit モックを完全に使わない方法は、やはり無理なようです。
Phake モック受け取り側の準備
受け取り側とは、テスト対象となる実際の Controller のことです。自分で実装する各種 Controller はController
を直接継承せずに間にAppController
を挟みます。このAppController
クラスに受け取り準備をしましょう。
class AppController extends Controller
{
public function beforeFilter()
{
parent::beforeFilter();
$class = Hash::merge(
$this->uses,
$this->helpers,
array_keys($this->components)
);
foreach ($class as $key) {
$this->{$key} = (ClassRegistry::isKeySet($key))
? ClassRegistry::getObject($key)
: $this->{$key};
}
}
}
$this->uses
$this->helpers
$this->components
これらはController
の public なプロパティです。CakePHP の作法ではここに文字列でクラス名を列挙します。コア実装によると、これらの生成はClassRegistry
でしていたようで、その手法を借りた形です。
$class
変数に一旦 Controller が使用する全てのクラス名をまとめます。そしてforeach
で個々のクラスに対して処理をします。ClassRegistry
はキー名のみ指定するほか、 事前にインスタンスの参照自体を渡す こともできます。この方法で Phake モック・インスタンスのやりとりをします。
上記injection()
でClassRegistry::addObject()
しなければ、ClassRegistry::isKeySet()
はfalse
を返します。そのときは手を加えずに CakePHP 本来の実装に頼りインスタンスを保持します。もしtrue
なら(つまりテスト中にinjection()
を用いたら)ここで Phake モックが介入できます。もし$class
に不正な文字列が紛れ込んでもisKeySet()
がfalse
な限り無害です。
独自クラスの受け取りについて
このままでは CakePHP サブクラスしか受け取ることができません。CakePHP に依存しないロジックをまとめたクラスなどは、AppController
ではなく該当 Controller 内で生成します。
public function beforeFilter()
{
parent::beforeFilter();
$this->Filter = (ClassRegistry::isKeySet('Filter'))
? ClassRegistry::getObject('Filter')
: new Filter;
}
Filter
は独自に作成したクラスを想定しています。new Filter
とベタ書きしていますが、ClassRegistry::getObject()
によって介入の余地を残しているため結合度は下がります。ただし先頭でApp::uses()
する必要は出てくるので、分離を徹底する場合はここで別のFactory
を挟むべきです。現在はちょっと妥協してます。
あとはテストを書くだけ
整いました。あとはテストを書くだけです。前回と準備内容は大きく異るもののインタフェースを変えていないので、テストメソッドは書き直さずそのまま使えます。
public function test_save_success()
{
// Setup
$any = Phake::anyParameters();
$Post = Phake::mock('Post');
Phake::when($Post)->save($any)->thenReturn(true);
$Filter = Phake::mock('Filter');
$dummy = Phake::partialMock('CommentObject');
Phake::when($Filter)->filterComment($any)->thenReturn($dummy);
Phake::when($Filter)->save($dummy) ->thenReturn(true);
$this->injection($Post, $Filter);
// Test
$this->testAction('/posts/edit/', ['method' => 'POST', 'data' => ['dummy']);
// Verify
Phake::verify($Post) ->save($any);
Phake::verify($Filter)->filterComment($any);
Phake::verify($Filter)->save($dummy);
}
ここだけ読むと分かりませんが、内部では PHPUnit モックを作成せず、いきなりClassRegistry
経由で Phake モックが使われています。すごい!
改訂版のメリット多し
前回は独自にFactoryComponent
を用意して、beforeFilter()
にも色々書いて…と手間が掛かっていました。掲載予定は無くなりましたが、準備していた「コンポーネント篇」ではさらにもう一手間増えていました。それが Model、Component、独自クラス、全てのクラスをただClassRegistry
でまかなうことが出来るのです。
準備が少なくなったので、ますます Phake モックテストを導入しやすくなったと思います。ぜひ、このテストの書き味を体験してみてください!
--
最後に、「ClassRegistry を使えばよくなる」と教えて下さった天の声の主様、ClassRegistry
についての解説を載せていたブログの方々に感謝します。もっと精進します。
--
2014/5/9 追記: 改めてPhake のモックで CakePHP をテスト コンポーネント篇を書きました。Component を単独でテストしたい場合はこちらです。
Discussion