CakePHPのWebTestCaseでfixtureを使う
Thursday, 1. October 2009, 11:57:02
経緯
CakePHPはユニットテスト実行時にテストデータを投入するfixture機能をサポートしています。CakePHPのユニットテストはSimpleTestをベースにしており、UnitTestCaseと、それを継承してWebブラウザ動作のテストを行うWebTestCase、2つのテストケースを持っています。CakeではそれぞれCakeTestCase、CakeWebTestCaseという名前になります。 ただしCakeWebTestCaseはWebTestCaseを継承しているだけで、何も機能拡張をしていません。 cake/tests/lib/cake_web_test_case.php
class CakeWebTestCase extends WebTestCase {
}
CakePHPオフィシャルのCookBookでも、以下のように解説されています。
しかし実際に開発の場面では、WebTestCaseを使って、ログインからの操作を試したり、データベースに依存した操作・結果を求めなければならない場面に遭遇します。そこで今回はfixutureを使えるようなWebTestCaseを作ってみます。CakeWebTestCase は、SimpleTest の WebTestCase をただ拡張したもので、特に機能追加はありません。SimpleTest の Web testing に関する文書中に記載がある全ての機能は、 CakeWebTestCase で利用できます。これはまた、 SimpleTest が持つ機能以外のものは使えないことを意味します。すなわち、 CakeWebTestCase においてフィクスチャは利用できず、テストケースにデータベースに対する更新や保存が含まれていた場合、恒久的にデータベースの値が変更されることを意味します。テストの結果は、しばしばデータベースが持つ値に基づくので、テスト手順の一部としてデータベースが期待した値を持つことを確認してください。
考察
今回fixtureを使えるWebTestCaseを作る上で、考慮したポイントは以下のとおりです。- なるべく書かない
- Cakeのバージョンが変わっても簡単に移行したい
- いつかは本体に組み込んで欲しい
実施
実施の手順は以下のとおりです。- cake_test_case.php をコピーして、 fixturable_web_base_test_case.php に変更
- 上記ファイルの「CakeTest」 部分を 「FixturableWebBaseTest」に変換
- FixturableWebBaseTestのスーパークラスをWebTestCaseに変更
- FixturableWebTestCase を作成
- bootstrap に初期化コードを追加
- 実際のテストケースを記述
1.cake_test_case.php をコピーして、 fixturable_web_base_test_case.php に変更
まず流用元のテストケースをコピして、継承する元になるクラスを作成します。
cd {アプリケーションのHOMEディレクトリ}
mkdir app/vendors/webtest
cp cake/tests/lib/cake_test_case.php app/vendors/webtest/fixturable_web_base_test_case.php
2.上記ファイルの「CakeTest」 部分を 「FixturableWebBaseTest」に変換
これはお使いのエディタを開いて、置換機能を使えばあっという間に完了です。 app/vendors/webtest/ fixturable_web_base_test_case.php を編集してください。3.FixturableWebBaseTestのスーパークラスをWebTestCaseに変更
このままでは、単にCakeTestCaseの名前を変更しただけなので、そのスーパークラスをUnitTestCaseからWebTestCaseに変更します。
class FixturableWebBaseTestCase extends WebTestCase {
// extends を変更する
}
4.FixturableWebTestCase を作成
手順3でWebTestCaseを使ったCakeTestCaseと似たものができました。ただしこのままではWebアクセス時にfixtureを読み込むようにはできません。 FixturableWebBaseTestを継承したクラスを作成します。このクラスでは、Webテスト中なのか、通常動作中なのかを判定するロジック(CakeTestCaseからの差分)を記述します。 またWebテスト中なのか、そうでないのかを判定するために、以下の方法を検討しました。- tmp/tests 以下のファイルで識別する
- UserAgentを使って識別する
- HTTP独自ヘッダで識別する
<?php
App::import('Vendor', 'webtest' . DS . 'fixturable_web_base_test_case');
/**
* FixturableWebTestCase class
*/
class FixturableWebTestCase extends FixturableWebBaseTestCase {
/**
* @overwrite
*/
function startCase() {
$this->_lockWebTesting();
}
/**
* @overwrite
*/
function endCase() {
$this->_unlockWebTesting();
}
/**
* bootstrap.php から呼び出す
*/
function initIfTestMode() {
if(file_exists(FixturableWebTestCase::_getLockFileName())) {
parent::_initDb();
Configure::write('Acl.database', 'test_suite');
}
}
/**
* TMPファイルを作成して、Webテスト中であることを宣言
*/
function _lockWebTesting() {
touch($this->_getLockFileName());
}
/**
* TMPファイルを削除して、Webテスト中でなくす
*/
function _unlockWebTesting() {
unlink($this->_getLockFileName());
}
function _getLockFileName() {
return TMP.'tests'.DS.'fixturable.web.test.tmp';
}
}
5.bootstrap に初期化コードを追加
最後の準備として、bootstrapに初期化コードを追加します。bootstrapはCakePHPで動作するすべてのアクションが必ず通過する最初のポイントなので、ここで利用するデータベースを切り替えるようにします。 app/config/bootstrap.php
if(($_SERVER['PHP_SELF'] != '/webroot/test.php') && Configure::read() > 0) {
if(App::import('Vendor', 'webtest' . DS . 'fixturable_web_test_case')) {
FixturableWebTestCase::initIfTestMode();
}
}
bootstrapはテストコードをtest.phpから実行しようとした場合も通過してしまうので、このURLはフィルタする必要があります。また通常運用で呼ばれないようにデバッグ値が0より大きい場合にデータベースの切り替えを実行するようにします。
6.実際のテストケースを記述
これでWebTestCaseでfixtureが使える準備は整いました。後はテストケースを記述するだけです。 今回サンプルとして、私の執筆した「CakePHPによる実践Webアプリケーション開発」で作ったCalendarNoteをテストしてみます。
<?php
App::import('Vendor', 'webtest' . DS . 'fixturable_web_test_case');
class UsersWebTest extends FixturableWebTestCase {
var $fixtures = array('app.group', 'app.user', 'app.users_group', 'app.schedule', 'app.schedules_user',
'app.aco', 'app.aro', 'app.aros_aco'
);
function startTest($method) {
parent::startTest($method);
Configure::write('Acl.database', 'test_suite');
$this->addHeader('Accept-Language:ja');
}
function testLoginAndCheckSchedule() {
$this->assertTrue($this->get('http://calendarnote.localhost/users/login'));
$this->assertTitle(new PatternExpectation('/CalendarNote/'));
$this->assertSubmit('Login');
$this->clickSubmit('Login', array(
'data[User][username]'=>'hide',
'data[User][password]'=>'password',
));
$this->assertText('Hidetoshi Nakata');
$this->assertLink('ログアウト');
$this->assertTrue($this->get('http://calendarnote.localhost/schedules/index/month/2009/01'));
$this->assertText('2008年12月28日');
$this->assertTrue($this->clickLink('10:00-12:00 Nengashiki'));
$this->assertFieldByName('data[Schedule][title]', 'Nengashiki');
$this->assertFieldByName('data[Schedule][contents]', 'In Japan, there are the New Year holidays and a New-Year\'s-greetings ceremony is performed to the first day of work.');
}
function endTest($method) {
parent::endTest($method);
$this->get('http://calendarnote.localhost/users/logout');
}
}
このテストシナリオは以下のとおりです。
- ログイン画面にアクセスできるか検証する
- タイトルがCalendarNoteになっているか検証する
- ログインボタンが出ているか検証する
- ユーザ名hide、パスワードpasswordでログイン(サブミット)する
- 次の画面に、フルネームが表示されていて、ログアウトリンクがあるか検証する。
- 2009年1月の月単位スケジュール一覧に遷移できることを検証する。
- 2008年12月28日という文字列の表示を検証する。
- スケジュール詳細へのリンクが表示されることを検証する。
- フォームのタイトルに正しい値が入っていることを検証する。
- フォームのコンテンツに正しい値が入っていることを検証する。
最後に
本カスタマイズはPHP5.2.10、CakePHP1.2.5で検証しています。CakePHP1.2系であれば特に問題なく動作すると思いますが、うまく動作しない場合はコメントいただけると助かります。 今後の展開としては、まずこの記事を英語にしてBakeryにアップしたいと思います。 それをきっかけにコア開発者の目にでもとまったら、本体への組み込みなんか検討してくれるかもしれません。 もしかして一度やろうとしてやめたのかもしれないんですけど、Cake祭り(*1)でコア開発者が日本に来るので、直接聞いてみたいなと思います。(*1) Cake祭り:昨年はCakePHPカンファレンスとして実施した、イベントの第2回目。今年もコア開発者が日本に来るので要注目のイベントです。今すぐ申し込みへ。 http://matsuri.cakephp.jp/








