Unit Testing CakePHP Shells | Mark Story
シェルの単体テストをどうやっていいかわからないなら、上の記事はなかなかおすすめです。テストしたいクラスとShellDispatcherをモック化する方法が紹介されています。
サンプルコードは、CakePHPに組み込まれているCoreテストの「/cake/tests/cases/console/libs/shell.test.php」にほぼ同じものがあります。筆者のMark Story氏も言っていますが、具体的な例が見たければそちらのソースコードを参照するといいでしょう。
以下、翻訳です。
シェルは単体テストをするのが難しいものの一つです。WEBコンテキストと違いCLIコンテキストの中で動作するため、うまくテストするのはちょっとしたチャレンジになるでしょう。特に高いハードルとなるのは、CLI(コマンドライン環境)とシェルを分離することと、正しい引数・パラメータをシミュレーションすることです。
環境依存要素の除去
シェルとCLIを分離するには、モックオブジェクトを使います。まずはテスト対象のシェルとShellDispatcherをモック化しましょう。ShellDispatcherをモッキングするのは特に重要です。以下、自作のクラス「AclExtrasShell」をテストする場合を例に説明します。シェルのテストでは、CLI環境と連動しているメソッドをモッキングする必要があります。
if (!defined('DISABLE_AUTO_DISPATCH')) {
define('DISABLE_AUTO_DISPATCH', true);
}
if (!class_exists('ShellDispatcher')) {
ob_start();
$argv = false;
require CAKE . 'console' . DS . 'cake.php';
ob_end_clean();
}
Mock::generatePartial(
'ShellDispatcher', 'AclExtrasMockShellDispatcher',
array('getInput', 'stdout', 'stderr', '_stop', '_initEnvironment')
);
入出力メソッドと _stop メソッドをモッキングすることで、ShellDispatcherからstdin(標準入力)とstdout(標準出力)を分離しました。また、テストケースがCLIコンテキストから独立して動作するようにしました。続いて、実際にテストしたいクラス内のいくつかのメソッドをモック化しましょう。
Mock::generatePartial(
'AclExtrasShell', 'MockAclExtrasShell',
array('in', 'hr', 'out', 'err', 'createFile', '_stop', 'getControllerList')
);
繰り返しますが、入力と出力に関わるメソッドはとても重要です。_stopもモック化することで、シェルが終了しようとしてもテストが継続できるようにします。これら二つのクラスをメソッドをモック化したら、テストメソッドの準備に取りかかりましょう。
テストケースの準備
シェルのテストと普通のテストケースは、なんら変わりません。ただしシェルの場合は少し違ったstartTestとendTestを必要とします。私はふだんは以下のようにします。
class AclExtrasShellTestCase extends CakeTestCase {
//...
function startTest() {
$this->Dispatcher =& new AclExtrasMockShellDispatcher();
$this->Task =& new MockAclExtrasShell($this->Dispatcher);
$this->Task->Dispatch =& $this->Dispatcher;
$this->Task->Dispatch->shellPaths = Configure::read('shellPaths');
}
//...
function endTest() {
unset($this->Task, $this->Dispatcher);
ClassRegistry::flush();
}
startTestの中で、モック化したShellDispatcherとテスト対象のシェルのインスタンスを作成します(4-5行目)。続いて手動でDispatcherとシェルをDispatchプロパティにアサインします(6行目)。さらにshellPathsを設定することで、オブジェクトとシェルのシミュレーションが可能になりました。endTestメソッドでは後掃除をし、テストの度に新しいオブジェクトを使えるようにします。注意として、もしシェル内でほかのシェルを利用している場合は、そちらもモック化などの準備をする必要があります。全部のDispatchの設定をするのもお忘れ無く。
in()とout()をアサートする
in()とout()はモック化されているので、代わりにsetReturnValue()とsetReturnValueAt()を使ってユーザーからの入力をシミュレートします。出力のアサーションにはexpectAt()を使います。例えば:
$this->Task->expectOnce('out');
$this->Task->expectAt(0, 'out', array(new PatternExpectation('/recovered/')));
「/recovered/」というパターンが一度出力されるであろう部分で、上記のように予想をセットします。呼び出される回数と個別に呼び出される部分を一緒に書くとうまくいくようです。個別に呼び出される部分だけ書いても、メソッドが呼ばれなかった場合はテストはfailになります。コール回数の予想だけだと、メソッドが呼ばれても何が起こっているのか知ることはできません。
さらに複雑な例
上記の通りやればうまくいきますが、もっと複雑なシェルのやりとりはどうすればいいでしょうか?そういう時はより細かいメソッドに分割すると良いでしょう。分割できないなら、より多くの手間を費やしてin()とout()の予想を書くほかないです。例として、ViewTaskのテストの一部分を取り上げましょう(bakeの一部分です)。
$this->Task->connection = 'test_suite';
$this->Task->args = array();
$this->Task->Controller->setReturnValue('getName', 'ViewTaskComments');
$this->Task->Project->setReturnValue('getAdmin', 'admin_');
$this->Task->setReturnValueAt(0, 'in', 'y');
$this->Task->setReturnValueAt(1, 'in', 'n');
$this->Task->setReturnValueAt(2, 'in', 'y');
$this->Task->expectCallCount('createFile', 4);
$this->Task->expectAt(0, 'createFile', array(
TMP . 'view_task_comments' . DS . 'admin_index.ctp',
new PatternExpectation('/ViewTaskComment/')
));
$this->Task->expectAt(1, 'createFile', array(
TMP . 'view_task_comments' . DS . 'admin_view.ctp',
new PatternExpectation('/ViewTaskComment/')
));
$this->Task->expectAt(2, 'createFile', array(
TMP . 'view_task_comments' . DS . 'admin_add.ctp',
new PatternExpectation('/Add ViewTaskComment/')
));
$this->Task->expectAt(3, 'createFile', array(
TMP . 'view_task_comments' . DS . 'admin_edit.ctp',
new PatternExpectation('/Edit ViewTaskComment/')
));
$this->Task->execute();
上記のテストでは複数のモックを使っています。また、どうやってシェルメソッドの予想とアサーションを書くかを示しています。PatternExpectationsをどう使うかと同様、モック化されたメソッドの入力をどうアサートするかわかるでしょう。
シェルクラスのテストが少しは楽になりましたか?もっと多くの例が知りたければ、coreのシェルクラスのテストケースを見るといいでしょう。
1 Comments.