2008.05.14 Wednesday
PHPUnitは基本的にはコマンドラインから実行するツールで、ZendFrameworkで作成されたControllerはWebサーバ経由で実行されるものなので、簡単にはテストができない。
Controllerのテストが単体テスト(Unit Test)と呼べるものかどうかという点については、ここでは議論しない。とにかく、ControllerをPHPUnitでテストしてみる、それだけだ。
通常のPHPUnitの使い方は、とてもわかりやすくまとまっているサイトがあるので、そちらを参照してもらうとしよう。
■PHPUnit参考サイト一覧(僕目線)
これで、PHPUnitを使って通常のクラスやメソッドの単体テストはできるようになったと思う。これを踏まえて、Controllerをテストする方法を考えた。いきなりコードを見てもらうのが一番だと思う。
まず、どういうディレクトリ構成にしているか、示しておこう。
.htaccess で一部の拡張子をのぞいたすべてのアクセスをindex.php宛になるようにRewriteしている。
そして、そのindex.phpで/home/www/zendapp/unitsample/controllers/以下のコントローラが呼び出されるようにしている。この辺はZendFrameworkでアプリを作成するときの標準的かつ基本的な部分なので、特に問題はないと思う。この環境下でテストを行うことを前提に話を進める。
/home/www/zendapp/unitsample/tests/Controller/IndexControllerTest.php
setUp()がすべて。
ZendFrameworkのドキュメントにあるZend_Controllerのところにも少しヒントが書いてあるんだけれど、要は、自分でリクエストオブジェクトを作成して、コマンドラインからのアクセスをあたかもWebサーバ経由のアクセスのように見せかけるところがポイントだ。
簡単に解説してみる。
Zend_Controller_Request_Httpのコンストラクタに渡すURIのドメイン部分は何でもよい。ここでは/sandbox/unitsampleというところが重要。実際にブラウザからアクセスする場合のURIを書くのが一番わかりやすいだろう。
重要なのはsetBaseUrlの部分だ。これがないと、sandboxがcontroller(module)名でunitsampleがmethod名だと判定されてしまう。そうではなくて、/sandbox/unitsample以下のindex(1つ目)がcontroller(module)名でindex(2つ目)がmethod名だと言うことを伝えているのがこの部分ということになる。いまは説明のために、コンストラクタに渡したURIをhttp://localhost/sandbox/unitsample/index/indexと書いたが、もちろんこれはindex/indexの部分を省略してhttp://localhost/sandbox/unitsampleと書いても良い。
そこから下の3行は環境によって若干異なるけれども、index.phpの記述とほとんど同じになるので、そこを参考にすれば良い。ちなみに、僕のindex.phpはこのようになっている。
/home/www/public_html/sandbox/unitsample/index.php
これのdispatch()の直前の3行が先ほどのテストスクリプトと同じになっていることがわかると思う。ちなみに、下記の2つのコードはほとんど同じなので、runを使っている方は、2つ目の書き方に脳内変換してからテストスクリプトを書いて頂ければと思う。
少し話がそれてしまったので、テストスクリプトのsetUp()の話に戻ろう。
次にob_start()とあるが、ここも大事なポイントだ。そのままdispatchをしてしまうと、実行結果(おそらくHTML)が標準出力に出力されてしまうので、これをテストすることができなくなってしまう。この出力をバッファリングして、最終的に変数に格納しようと言うのがこの部分だ。やっていることは単純なので、どうしてそうなるのかは、ob_starやob_get_cleanのドキュメントなどを見てもらいたい。
これで$this->contents にこのコントローラの実行結果が格納される。あとは、このデータを使ってテストを行えばいいわけだ。参考に、簡単なテストをひとつ書いておいた。出力にちゃんとhtmlタグが含まれるかどうかのテストだ。あまり厳密なテストではないが、サンプルとしては十分だろう。同様にテストメソッドを追加して、$this->contents に対して期待した結果が含まれるかどうかをいくつか書けば、立派なテストになるだろう。
実行結果
ひとつ注意しておきたいのが、テストメソッドそれぞれが実行されるたびに、setUp()は呼ばれるということだ。$this->contents の中身はおそらく変わらないであろうが、setUp()をテスト間で共有する意味はあまりないというようなことがPHPUnitのポケットガイドに書かれていたので、ここはあえてそうしている。ただ、そういう風になっているということは知っておいた方が良いと思う。
さらに、引数をつけてアクセスした時のテストが必要になる場合(実際はそういう方が多いだろうが)は、setUp()を使って共通化することはせずに、それぞれのテストメソッドの中で引数をつけたURIでZend_Controller_Request_Httpをnewすればよい。
ちょと駆け足になってしまったけど、これで少しでもテストをする気が起きてくれれば良いと思う。
Controllerのテストが単体テスト(Unit Test)と呼べるものかどうかという点については、ここでは議論しない。とにかく、ControllerをPHPUnitでテストしてみる、それだけだ。
通常のPHPUnitの使い方は、とてもわかりやすくまとまっているサイトがあるので、そちらを参照してもらうとしよう。
■PHPUnit参考サイト一覧(僕目線)
- PHPUnit3で始めるユニットテスト
(Do you PHP? の方が書かれている記事) - PHPUnit を使ってユニットテスト - Heavens hell
(PHP Framework Fight! にZendFrameworkで参戦してくれる方。要応援) - PHPUnit ポケットガイド
(PHPUnit のガイド的なあれ)
これで、PHPUnitを使って通常のクラスやメソッドの単体テストはできるようになったと思う。これを踏まえて、Controllerをテストする方法を考えた。いきなりコードを見てもらうのが一番だと思う。
まず、どういうディレクトリ構成にしているか、示しておこう。
/home/www/public_html/sandbox/unitsample/.htaccess
/home/www/public_html/sandbox/unitsample/index.php
/home/www/zendapp/unitsample/controllers/IndexController.php
/home/www/zendapp/unitsample/tests/Controller/IndexControllerTest.php
.htaccess で一部の拡張子をのぞいたすべてのアクセスをindex.php宛になるようにRewriteしている。
そして、そのindex.phpで/home/www/zendapp/unitsample/controllers/以下のコントローラが呼び出されるようにしている。この辺はZendFrameworkでアプリを作成するときの標準的かつ基本的な部分なので、特に問題はないと思う。この環境下でテストを行うことを前提に話を進める。
/home/www/zendapp/unitsample/tests/Controller/IndexControllerTest.php
<?
require_once 'PHPUnit/Framework.php';
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/Request/Http.php';
class Controller_IndexControllerTest extends PHPUnit_Framework_TestCase {
protected $app_dir = '/home/www/zendapp/unitsample';
protected $contents;
protected function setUp() {
$req = new Zend_Controller_Request_Http('http://localhost/sandbox/unitsample/index/index');
$req->setBaseUrl('/sandbox/unitsample');
$front = Zend_Controller_Front::getInstance();
$front->setParam('noViewRenderer', true);
$front->setControllerDirectory( $this->app_dir.'/controllers' );
ob_start();
$response = $front->dispatch();
$this->contents = ob_get_clean();
}
public function testIndexAction_has_html_tag(){
$contents = $this->contents;
$this->assertRegExp('/<html.*>/', $contents, 'html開始タグが存在しません。');
$this->assertRegExp('/<¥/html>/', $contents, 'html終了タグが存在しません。');
}
}
setUp()がすべて。
ZendFrameworkのドキュメントにあるZend_Controllerのところにも少しヒントが書いてあるんだけれど、要は、自分でリクエストオブジェクトを作成して、コマンドラインからのアクセスをあたかもWebサーバ経由のアクセスのように見せかけるところがポイントだ。
簡単に解説してみる。
Zend_Controller_Request_Httpのコンストラクタに渡すURIのドメイン部分は何でもよい。ここでは/sandbox/unitsampleというところが重要。実際にブラウザからアクセスする場合のURIを書くのが一番わかりやすいだろう。
重要なのはsetBaseUrlの部分だ。これがないと、sandboxがcontroller(module)名でunitsampleがmethod名だと判定されてしまう。そうではなくて、/sandbox/unitsample以下のindex(1つ目)がcontroller(module)名でindex(2つ目)がmethod名だと言うことを伝えているのがこの部分ということになる。いまは説明のために、コンストラクタに渡したURIをhttp://localhost/sandbox/unitsample/index/indexと書いたが、もちろんこれはindex/indexの部分を省略してhttp://localhost/sandbox/unitsampleと書いても良い。
そこから下の3行は環境によって若干異なるけれども、index.phpの記述とほとんど同じになるので、そこを参考にすれば良い。ちなみに、僕のindex.phpはこのようになっている。
/home/www/public_html/sandbox/unitsample/index.php
<?
$app_dir = '/home/www/zendapp/unitsample';
require_once 'Zend/Controller/Front.php';
$front = Zend_Controller_Front::getInstance();
$front->setParam('noViewRenderer', true);
$front->setControllerDirectory( $app_dir.'/controllers' );
$front->dispatch();
これのdispatch()の直前の3行が先ほどのテストスクリプトと同じになっていることがわかると思う。ちなみに、下記の2つのコードはほとんど同じなので、runを使っている方は、2つ目の書き方に脳内変換してからテストスクリプトを書いて頂ければと思う。
$front = Zend_Controller_Front::run( $app_dir.'/controllers' );
$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory( $app_dir.'/controllers' );
$front->dispatch();
少し話がそれてしまったので、テストスクリプトのsetUp()の話に戻ろう。
次にob_start()とあるが、ここも大事なポイントだ。そのままdispatchをしてしまうと、実行結果(おそらくHTML)が標準出力に出力されてしまうので、これをテストすることができなくなってしまう。この出力をバッファリングして、最終的に変数に格納しようと言うのがこの部分だ。やっていることは単純なので、どうしてそうなるのかは、ob_starやob_get_cleanのドキュメントなどを見てもらいたい。
これで$this->contents にこのコントローラの実行結果が格納される。あとは、このデータを使ってテストを行えばいいわけだ。参考に、簡単なテストをひとつ書いておいた。出力にちゃんとhtmlタグが含まれるかどうかのテストだ。あまり厳密なテストではないが、サンプルとしては十分だろう。同様にテストメソッドを追加して、$this->contents に対して期待した結果が含まれるかどうかをいくつか書けば、立派なテストになるだろう。
実行結果
$ cd /home/www/zendapp/unitsample/tests
$ phpunit Controller_IndexControllerTest
PHPUnit 3.1.9 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 tests)
ひとつ注意しておきたいのが、テストメソッドそれぞれが実行されるたびに、setUp()は呼ばれるということだ。$this->contents の中身はおそらく変わらないであろうが、setUp()をテスト間で共有する意味はあまりないというようなことがPHPUnitのポケットガイドに書かれていたので、ここはあえてそうしている。ただ、そういう風になっているということは知っておいた方が良いと思う。
さらに、引数をつけてアクセスした時のテストが必要になる場合(実際はそういう方が多いだろうが)は、setUp()を使って共通化することはせずに、それぞれのテストメソッドの中で引数をつけたURIでZend_Controller_Request_Httpをnewすればよい。
ちょと駆け足になってしまったけど、これで少しでもテストをする気が起きてくれれば良いと思う。