fuelPHPでユニット・コントローラーテストしようと思ったら意外に情報がなかったのでまとめました。
#前準備
##PHPUnitのインストール
プロジェクトのcomposer.json
のrequireにphpunitを追加します。
"require": {
"php": ">=5.3.3",
"composer/installers": "~1.0",
"fuel/docs": "1.7.2",
"fuel/core": "1.7.2",
"fuel/auth": "1.7.2",
"fuel/email": "1.7.2",
"fuel/oil": "1.7.2",
"fuel/orm": "1.7.2",
"fuel/parser": "1.7.2",
"fuelphp/upload": "2.0.1",
"monolog/monolog": "1.5.*",
"michelf/php-markdown": "1.4.0",
"phpunit/phpunit": "3.7.*" ← 追加
}
##composer経由でPHPUnitをインストール
composerを実行してPHPUnitをインストールします。
$ php composer.phar install
oilコマンドから実行可能にしておきます。
<?php
return array(
'phpunit' => array(
'binary_path' => 'fuel/vendor/phpunit/phpunit/composer/bin/phpunit',
),
);
#ユニットテスト
コントローラーのテストを行う前に、ユニットテストをやってみます。
メンバーモデルにIDとパスワードプロパティが存在し、どちらのプロパティもrequiredなケースを考えます。
$ oil g scaffold member member_id:varchar[32] password:varchar[32]
$ oil refine migrate
生成されたmember
モデルを見てみます。
use Orm\Model;
class Model_Member extends Model
{
protected static $_properties = array(
'id',
'member_id',
'password',
'created_at',
'updated_at',
);
protected static $_observers = array(
'Orm\Observer_CreatedAt' => array(
'events' => array('before_insert'),
'mysql_timestamp' => false,
),
'Orm\Observer_UpdatedAt' => array(
'events' => array('before_save'),
'mysql_timestamp' => false,
),
);
public static function validate($factory)
{
$val = Validation::forge($factory);
$val->add_field('member_id', 'Member Id', 'required|max_length[32]');
$val->add_field('password', 'Password', 'required|max_length[32]');
return $val;
}
}
member_id
とpassword
がrequiredになっているのがわかります。
※ 実際にはパスワードは暗号化しないとダメですが、便宜的に平文とします。
この状態でmember
のモデルテストを書いてみます。
<?php
/**
* Model Post class tests
*
* @group Model
* @group Member
*/
class Test_Model_Member extends \TestCase
{
public function test_create_member(){
$count = count(Model_Member::find("all"));
$member = Model_Member::forge(array(
'member_id' => "member_id",
'password' => 'password',
));
$member->save();
$update_count = count(Model_Member::find("all"));
$this->assertEquals($count+1,$update_count);
}
}
テストを実行します。
$ php oil test
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
............................................................... 63 / 376 ( 16%)
............................................................... 126 / 376 ( 33%)
............................................................... 189 / 376 ( 50%)
............................................................... 252 / 376 ( 67%)
............................................................... 315 / 376 ( 83%)
.............................................................
Time: 454 ms, Memory: 19.50Mb
OK (376 tests, 448 assertions)
通りました。
ここで、試しにmember_id
のnull
を許容してみます。
magic migration
ファイルを作ります。
fuelPHPのmagic migration
には、カラムの属性だけを変えるmigrationがないので、rename
のmagic migration
をベースにします。
$ oil g migration rename_field_member_id_to_member_id_in_members
Creating migration: /home/unko/sample/fuel/app/migrations/002_rename_field_member_id_to_member_id_in_members.php
null制約をはずす
<?php
namespace Fuel\Migrations;
class Rename_field_member_id_to_member_id_in_members
{
public function up()
{
\DBUtil::modify_fields('members', array(
'member_id' => array('name' => 'member_id', 'type' => 'varchar', 'constraint' => 32, 'null' => true) //← nullを許容
));
}
public function down()
{
\DBUtil::modify_fields('members', array(
'member_id' => array('name' => 'member_id', 'type' => 'varchar', 'constraint' => 32)
));
}
}
実行します。
$ oil r migrate
Performed migrations for app:default:
002_rename_field_member_id_to_member_id_in_members
この状態で、先ほどのtests/model/member.php
を修正し、member_id
を設定しないでテストを実行します。
class Test_Model_Member extends \TestCase
{
public function test_create_member(){
$count = count(Model_Member::find("all"));
$member = Model_Member::forge(array(
// 'member_id' => "member_id", ← member_idをコメントアウト
'password' => 'password',
));
$member->save();
$update_count = count(Model_Member::find("all"));
$this->assertEquals($count+1,$update_count);
}
}
members
テーブルはmember_id
はnull
を許容しますが、member
モデルのmember_id
はrequiredなのでバリデーションエラーになるはず。
$ php oil test
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
............................................................... 63 / 376 ( 16%)
............................................................... 126 / 376 ( 33%)
............................................................... 189 / 376 ( 50%)
............................................................... 252 / 376 ( 67%)
............................................................... 315 / 376 ( 83%)
.............................................................
Time: 440 ms, Memory: 19.75Mb
OK (376 tests, 448 assertions)
ファッ?!
テーブルを見てみると・・
mysql> select * from members;
+----+-----------+----------+------------+------------+
| id | member_id | password | created_at | updated_at |
+----+-----------+----------+------------+------------+
| 1 | member_id | password | 1405664980 | 1405664980 |
| 2 | member_id | password | 1405664990 | 1405664990 |
| 3 | member_id | password | 1405664998 | 1405664998 |
| 4 | NULL | password | 1405665773 | 1405665773 |← ?!
+----+-----------+----------+------------+------------+
~~実はfuelPHPはモデルとバリデーションが紐づいていないため、このユニットテストではバリデーションがチェックできません。~~ORMの$_properties
にバリデーションを追加し、Fieldset::add_model
したモデルを使用することでモデルとバリデーション、フォームを紐づけることができますが、今回はscaffoldしたソースを使用しているため、モデルとバリデーションが紐づいていません。
member
のコントローラーの記述からもそのことがわかります。
public function action_create()
{
if (Input::method() == 'POST')
{
$val = Model_Member::validate('create');
if ($val->run()) //←ここでバリデーション実行
{
$member = Model_Member::forge(array(
'member_id' => Input::post('member_id'),
'password' => Input::post('password'),
));
if ($member and $member->save()) //←モデル保存
{
Session::set_flash('success', 'Added member #'.$member->id.'.');
Response::redirect('member');
}
else
{
Session::set_flash('error', 'Could not save member.');
}
}
else
{
Session::set_flash('error', $val->error());
}
}
$this->template->title = "Members";
$this->template->content = View::forge('member/create');
}
ではfuelPHPでバリデーションの単体テストをするにはどうするか?
バリデーション専用のテストを別途用意する必要があります。
//テスト追加
public function test_validation_member(){
$val = Model_Member::validate('create');
$member = array(
'password' => 'password',
);
$res = $val->run($member);
$this->assertEquals($res,false);
}
テストを実行します。
$ php oil test
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
............................................................... 63 / 377 ( 16%)
............................................................... 126 / 377 ( 33%)
............................................................... 189 / 377 ( 50%)
............................................................... 252 / 377 ( 66%)
............................................................... 315 / 377 ( 83%)
..............................................................
Time: 443 ms, Memory: 19.75Mb
OK (377 tests, 449 assertions)
~~モデルのユニットテストにバリデーションを含めるのが正しいのかどうかはさておき、~~バリデーションのチェックとモデルのユニットテストが確認できました。
#コントローラーテスト
次にコントローラーのアクションごとの機能テストを行いたいと思います。
#前準備
fuelPHP界隈では有名な話らしいのですが、デフォルトのソースだと、リダイレクト時にexitされてしまってunitTestごと終了するのでテストがうまく動いてくれません。
ためしに存在しないidでviewアクションを呼び出すテストを書いてみます。
<?php
/**
* Controller Post class tests
*
* @group Controller
* @group Member
*/
class Test_Controller_Member extends \TestCase
{
public function test_create_member(){
$response = Request::forge('member/view')
->set_method('GET')
->execute(["id"=>100])
->response();
}
}
実行します。
$ php oil test group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
...[unko@localhost sample]$
テスト自体が途中で終了してしまいました。
これではテストにならないので、回避するためのフック用パッケージを作成する必要があります。
フックパッケージはこちらを参考にさせていただきました。
FuelPHP【チュートリアル】-- コントローラーのテストの方法 --
##コントローラテストのみが使用するパッケージを作成する
oilコマンドを使ってパッケージのひな形を作成します。
$ oil g package my
Creating file: /home/unko/sample/fuel/packages/my/classes/my.php
Creating file: /home/unko/sample/fuel/packages/my/config/my.php
Creating file: /home/unko/sample/fuel/packages/my/bootstrap.php
classes/my.php
を開き、redirectメソッドをオーバーライドします。
<?php
namespace My;
class MyException extends \FuelException {}
class Response extends \Fuel\Core\Response
{
/**
* Override Fuel\Core\Response redirect method
*
* @param string $url The url
* @param string $method The redirect method
* @param int $code The redirect status code
*
* @return void
*/
public static function redirect($url = '', $method = 'location', $code = 302)
{
$response = new static;
$response->set_status($code);
if (strpos($url, '://') === false)
{
$url = $url !== '' ? \Uri::create($url) : \Uri::base();
}
strpos($url, '*') !== false and $url = \Uri::segment_replace($url);
if ($method == 'location')
{
$response->set_header('Location', $url);
}
elseif ($method == 'refresh')
{
$response->set_header('Refresh', '0;url='.$url);
}
else
{
return;
}
if (\Fuel\Core\Fuel::$env != 'test')
{
$response->send(true);
exit;
}
$response->send(true);
}
}
合わせてbootstrap.php
も修正します。
<?php
Autoloader::add_core_namespace('My');
Autoloader::add_classes(array(
'My\\Response' => __DIR__ . '/classes/my.php',
'My\\MyException' => __DIR__ . '/classes/my.php',
));
このパッケージはcore
の挙動をオーバーライドするので、本番環境には含めず、config.php
のalways_load
には含めず、テスト環境のみで使用したいと思います。
PHPUnitのみmy
パッケージを使用させるようにbootstrap_phpunit.php
をプロジェクト用にコピーし、それを修正します。
$ cp fuel/core/bootstrap_phpunit.php fuel/app/tests/
// 末尾に追加
Package::load('my');
phpunit.xml
もプロジェクト専用のものを用意します。
$ cp fuel/core/phpunit.xml fuel/app/
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true" stopOnFailure="false" bootstrap="./tests/bootstrap_phpunit.php"> ← プロジェクト専用のbootstrap_phpunit.phpに変更
<php>
<server name="doc_root" value="fuel/public"/>
<server name="app_path" value="fuel/app"/>
<server name="core_path" value="fuel/core"/>
<server name="package_path" value="fuel/packages"/>
<server name="vendor_path" value="fuel/vendor"/>
<server name="FUEL_ENV" value="test"/>
</php>
<testsuites>
<testsuite name="core">
<directory suffix=".php">../core/tests</directory>
</testsuite>
<testsuite name="packages">
<directory suffix=".php">../packages/*/tests</directory>
</testsuite>
<testsuite name="app">
<directory suffix=".php">../app/tests</directory>
</testsuite>
<testsuite name="modules">
<directory suffix=".php">../app/modules/*/tests</directory>
</testsuite>
</testsuites>
</phpunit>
テストを再実行します。
$ php oil test group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
....
Time: 253 ms, Memory: 16.00Mb
OK (3 tests, 0 assertions)
今度は正常に完了しました。
リダイレクトとは直接関係ありませんが、テストから呼び出されるViewはなぜかAssetクラスが動いてくれないので、パス解決のエラー表示を無効にしておきます。
'fail_silently' => true, ← doc_rootが正しく指定されていればこの処理は不要
phpunit.xmlのdoc_rootが誤っていたため発生してました。doc_rootを正しくfuel/public
に設定していればこの箇所は不要です。
##viewアクションのコントローラーテストを行う
リダイレクトしてもテストが終了しなくなったので、viewアクションを呼び出し、存在しているIDならば詳細画面を、そうでなければリダイレクトするテストを行います。
テストファイルを書き換えます。
public function test_view_member(){
$response = Request::forge('member/view')
->set_method('GET')
->execute(["id" => 1]) //← 存在するIDに
->response();
$this->assertTag(array('tag' => 'span','class' => 'muted', 'content' =>'#1'),$response->body->__toString()); //← 存在する場合、表示されるべきHTMLタグのアサーション
}
assertTag
メソッドを使い、正常系であれば表示されるはずのフラッシュメッセージがHTMLに含まれているかチェックします。
テストを実行します。
$ php oil test group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/core/phpunit.xml
....
Time: 246 ms, Memory: 16.25Mb
OK (3 tests, 1 assertion)
成功しました。
次に、DBに登録されていないidでmemberのviewにアクセスした場合のテストを行います。
member
コントローラーは、リダイレクトが発生するとtemplateにコンテンツを設定せずに終了するため、assertTagsによるチェックができません。
なのでリダイレクトされたかどうかを判定するための専用プロパティをmy.php
に追加します。
<?php
namespace My;
class MyException extends \FuelException {}
class Response extends \Fuel\Core\Response
{
/**
* Override Fuel\Core\Response redirect method
*
* @param string $url The url
* @param string $method The redirect method
* @param int $code The redirect status code
*
* @return void
*/
public static function redirect($url = '', $method = 'location', $code = 302)
{
$response = new static;
$response->set_status($code);
if (strpos($url, '://') === false)
{
$url = $url !== '' ? \Uri::create($url) : \Uri::base();
}
strpos($url, '*') !== false and $url = \Uri::segment_replace($url);
if ($method == 'location')
{
$response->set_header('Location', $url);
}
elseif ($method == 'refresh')
{
$response->set_header('Refresh', '0;url='.$url);
}
else
{
return;
}
//テストではない場合はexitを実行
if (\Fuel\Core\Fuel::$env != 'test')
{
$response->send(true);
exit;
}
Response::$redirect_status = $code; //←ローカル変数の$codeをスタティックメンバに代入
Response::$redirect_url = $url; //←ローカル変数の$urlをスタティックメンバに代入
$response->send(true);
}
private static $redirect_status = null;// ← リダイレクト時のステータスコードを参照するためのメンバを追加
private static $redirect_url =""; //← リダイレクト時のURLを参照するためのメンバを追加
public static function get_redirect_status(){ //← リダイレクト時のステータスコードを参照するためのゲッター
return Response::$redirect_status;
}
public static function get_redirect_url(){ //← リダイレクト時のURLを参照するためのゲッター
return Response::$redirect_url;
}
}
存在しないメンバIDを指定し、member_index
へリダイレクトするテストを追加します。
// テストを追加
public function test_view_member_was_nothing(){
$response = Request::forge('member/view')
->set_method('GET')
->execute(["id" => 999999])
->response();
$this->assertEquals("302",Response::get_redirect_status()); //← 追加した専用メソッドでステータスコードをチェック
$this->assertEquals("member",Response::get_redirect_url());//← 専用メソッドでリダイレクト先のURLをチェック
}
テストを実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
....
Time: 278 ms, Memory: 17.50Mb
OK (5 tests, 3 assertions)
成功しました。
ここまでで、存在するIDがあった場合はmember詳細を、なければインデックスにリダイレクトされるところまで、コントローラーのテストができたことになります。
##isnertアクションのコントローラーテストを行う
今度はコントローラー経由でmemberを新規登録し、誤った値が入力されたらエラーになるテストを行います。
member追加の正常系テストを記述します。
// insertのテストを追加
public function test_create_member(){
$count = count(Model_Member::find("all"));
$_POST["member_id"] = "test_user"; //← 入力値を$_POSTで直接指定
$_POST["password"] = "password";
$response = Request::forge('member/create')
->set_method('POST')
->execute()
->response();
$this->assertTag(array('tag' => 'strong', 'content' =>'Success'),$response->body->__toString()); //← 正常終了時のフラッシュメッセージ
$this->assertEquals($count+1,count(Model_Member::find("all"))); //← insertが実行され、レコードが1件追加になっていることを確認
}
実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
......
Time: 274 ms, Memory: 17.50Mb
OK (6 tests, 5 assertions)
正常に終了しました。
今度はパスワードを未入力とし、バリデーションエラーになるテストを実行します。
// バリデーションエラーテストを追加
public function test_create_member_error(){
$count = count(Model_Member::find("all"));
$_POST["member_id"] = "test_user";
// わざとpasswordを指定しない
$response = Request::forge('member/create')
->set_method('POST')
->execute()
->response();
$this->assertTag(array('tag' => 'strong', 'content' =>'Error'),$response->body->__toString());
$this->assertEquals($count,count(Model_Member::find("all")));
}
実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
......E
Time: 276 ms, Memory: 17.50Mb
There was 1 error:
1) Test_Controller_Member::test_create_member_error
DomainException: Form instance already exists, cannot be recreated. Use instance() instead of forge() to retrieve the existing instance.
/home/unko/sample/fuel/core/classes/validation.php:57
/home/unko/sample/fuel/app/classes/model/member.php:27
/home/unko/sample/fuel/app/classes/controller/member.php:32
/home/unko/sample/fuel/core/classes/request.php:444
/home/unko/sample/fuel/app/tests/controller/member.php:51
FAILURES!
Tests: 7, Assertions: 5, Errors: 1.
例外が発生してしまいました。メッセージを詳しく見てみます。
DomainException: Form instance already exists, cannot be recreated. Use instance() instead of forge() to retrieve the existing instance.
/home/unko/sample/fuel/core/classes/validation.php:57
/home/unko/sample/fuel/app/classes/model/member.php:27
~~静的呼び出しをしたつもりが、instanceを使用しろと言われるのもちょっとよくわからないんですが、~~fuelPHPではcore
のvalidation.php
と、それが依存しているfieldset.php
で遅延静的束縛を使用している個所があり、連続してvalidation
をrun
するテストメソッドを実行すると、以前にnew static
したクラスが初期化されず、例外を発生させているようです。
これを回避するために、遅延静的束縛されているプロパティを初期化するメソッドを追加します。
ところでnew staticって記述、ちょっと違和感があるんですがこれって普通なんですかね。
<?php
namespace My;
class Fieldset extends \Fuel\Core\Fieldset {
//fieldsetの$_instancesを初期化するメソッド
public static function reset(){
parent::$_instances = array();
}
}
my
パッケージのbootstrap.php
にfieldset_ex.php
を追加します。
<?php
Autoloader::add_core_namespace('My');
Autoloader::add_classes(array(
'My\\Response' => __DIR__ . '/classes/my.php',
'My\\MyException' => __DIR__ . '/classes/my.php',
'My\\Fieldset' => __DIR__ . '/classes/fieldset_ex.php', //← 追加
));
テストtest_create_member_error
のRequest::execute()
よりも前にFieldset::reset()
を追加します。
public function test_create_member_error(){
Fieldset::reset(); //←追加
$count = count(Model_Member::find("all"));
$_POST["member_id"] = "test_user2";
$response = Request::forge('member/create')
->set_method('POST')
->execute()
->response();
$this->assertTag(array('tag' => 'strong', 'content' =>'Error'),$response->body->__toString());
$this->assertEquals($count,count(Model_Member::find("all")));
}
テストを再実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
......E
Time: 290 ms, Memory: 17.50Mb
There was 1 error:
1) Test_Controller_Member::test_create_member_error
Fuel\Core\Database_Exception: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'password' cannot be null with query: "INSERT INTO `members` (`member_id`, `password`, `created_at`, `updated_at`) VALUES ('test_user2', null, 1406185977, 1406185977)"
/home/unko/sample/fuel/core/classes/database/pdo/connection.php:270
/home/unko/sample/fuel/core/classes/database/pdo/connection.php:237
/home/unko/sample/fuel/core/classes/database/query.php:287
/home/unko/sample/fuel/packages/orm/classes/query.php:1424
/home/unko/sample/fuel/packages/orm/classes/model.php:1357
/home/unko/sample/fuel/packages/orm/classes/model.php:1301
/home/unko/sample/fuel/app/classes/controller/member.php:41
/home/unko/sample/fuel/core/classes/request.php:444
/home/unko/sample/fuel/app/tests/controller/member.php:53
Caused by
PDOException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'password' cannot be null
/home/unko/sample/fuel/core/classes/database/pdo/connection.php:237
/home/unko/sample/fuel/core/classes/database/query.php:287
/home/unko/sample/fuel/packages/orm/classes/query.php:1424
/home/unko/sample/fuel/packages/orm/classes/model.php:1357
/home/unko/sample/fuel/packages/orm/classes/model.php:1301
/home/unko/sample/fuel/app/classes/controller/member.php:41
/home/unko/sample/fuel/core/classes/request.php:444
/home/unko/sample/fuel/app/tests/controller/member.php:53
FAILURES!
Tests: 7, Assertions: 5, Errors: 1.
今度はINSERTのNULL制約でエラーが発生しました。
fieldset
のinstances
を初期化してもfieldset
が内部で使用している$input
も遅延静的束縛となっているため、$input
と$_POST
の内容にアンマッチが発生し、バリデーションを通過してSQLエラーになるようです。
これを解決するため、$input
をリセットするためのメソッドを追加します。
ただしfuelPHPのcore/Inputクラスは拡張できないので、Inputを継承したInputEXクラスを追加します。
<?php
namespace My;
class InputEX extends \Fuel\Core\Input {
public static function reset(){
parent::$input = null;
}
}
my
パッケージのbootstrap.php
にinput_ex.php
を追加します。
<?php
Autoloader::add_core_namespace('My');
Autoloader::add_classes(array(
'My\\Response' => __DIR__ . '/classes/my.php',
'My\\MyException' => __DIR__ . '/classes/my.php',
'My\\Fieldset' => __DIR__ . '/classes/fieldset_ex.php',
'My\\InputEx' => __DIR__ . '/classes/input_ex.php', //← 追加
));
テストtest_create_member_error
にInputEx::reset()
を追加します。
public function test_create_member_error(){
InputEx::reset(); //← 追加
Fieldset::reset();
$count = count(Model_Member::find("all"));
$_POST["member_id"] = "test_user2";
$response = Request::forge('member/create')
->set_method('POST')
->execute()
->response();
$this->assertTag(array('tag' => 'strong', 'content' =>'Error'),$response->body->__toString());
$this->assertEquals($count,count(Model_Member::find("all")));
}
テストを実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
.......
Time: 278 ms, Memory: 17.75Mb
OK (7 tests, 7 assertions)
通りました!
この状態だとテスメソッドすべてにreset
の呼び出しを記述する必要があり、あまりいけてないのでsetUp()
メソッドで処理するように変更します。
//setUpメソッドを追加
protected function setUp(){
Session::destroy(); //← ついでにセッションの初期化も追加
Request::reset_request(true); //← ついでにリクエストデータの初期化も追加
InputEx::reset();
Fieldset::reset();
}
これでインサート、バリデーション、リダイレクトのコントローラーテストができる環境が整いました!
##editのコントローラーテストを行う
続いて既存のデータに対して更新のテストを行います。
既存データの更新テストを行うためには、データベースを既定の状態に設定する必要がありますが、fuelPHPおよびPHPUnitにはそのような機能がありません。
しかしながらFuelPHP での PHPUnit によるユニットテストでフィクスチャ機能を実装されている方がいますので、これを使わせてもらいます。
FuelPHP での PHPUnit によるユニットテストの手順に沿ってdbfixt
タスクを作成します。
タスクを実行し、既存のテーブルからフィクスチャデータを生成します。
$ oil r dbfixt:generate members
成功するとfuel/app/tests/fixture
ディレクトリにテーブル名_fixt.yml
というファイルが生成されます。
---
-
id: 1
member_id: member_id
password: password
created_at: 1405664980
updated_at: 1405664980
-
id: 2
member_id: member_id
password: password
created_at: 1405664990
updated_at: 1405664990
-
id: 3
member_id: member_id
password: password
created_at: 1405664998
updated_at: 1405664998
同様にテストファイル内の$tables
で指定されたテーブルのデータをymlの内容で更新するためクラスをfuel/app/classes/dbtestcase.php
に追加します。
毎テスト実行時にmembers
テーブルをmembers_fixt.yml
の内容で行使するため、テストファイルを修正します。
<?php
/**
* Controller Post class tests
*
* @group Controller
* @group Member
*/
class Test_Controller_Member extends \DbTestCase //← 継承元をDbTestCaseに変更
{
protected $tables = ['members' => 'members']; //← fixture.ymlの内容で更新するテーブルを記述
protected function setUp(){
Request::reset_request(true);
InputEx::reset();
Fieldset::reset();
parent::setUp(); //← DbTestCaseのsetUp()をコールするために追加
}
これで毎回テストを実行する前にmembers
テーブルのデータがmembers_fixt.yml
の内容でリセットされるようになります。
取得したデータを更新するテストと、更新に失敗するテストを作成してみます。
fuelPHPのORMモデルはオブジェクトをキャッシュするようなので、変更後のレコードはORMを使用せず、クエリビルダで取得しています。
//更新テスト 正常系
public function test_update_member(){
$update_member_id = "test_user_updated";
$id = 1;
$_POST["member_id"] = $update_member_id;
$_POST["password"] = "password";
$response = Request::forge('member/edit')
->set_method('POST')
->execute(["id" => $id])
->response();
$updated_user = DB::select()->from("members")->where("id",$id)->execute(); //← クエリビルダを使用
$this->assertEquals($update_member_id,$updated_user[0]["member_id"]);
$this->assertEquals("302",Response::get_redirect_status());
}
//更新テスト エラー系
public function test_update_member_error(){
$update_member_id = "test_user_updated";
$id = 3;
$_POST["member_id"] = $update_member_id;
$_POST["password"] = "123456789012345678901234567890123";
$response = Request::forge('member/edit')
->set_method('POST')
->execute(["id" => $id])
->response();
$updated_user = DB::select()->from("members")->where("id",$id)->execute(); //← クエリビルダを使用
$this->assertEquals("member_id",$updated_user[0]["member_id"]);
$this->assertTag(array('tag' => 'strong', 'content' =>'Error'),$response->body->__toString());
$this->assertTag(array('tag' => 'div', 'attributes' => array('class' =>'alert'), 'descendant' => array('tag' => 'p', 'content' => 'The field Password may not contain more than 32 characters.')),$response->body->__toString());
}
実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
..........
Time: 451 ms, Memory: 18.25Mb
OK (10 tests, 12 assertions)
通りました。
assertTagsの使用により、エラー文言('The field Password may not contain more than 32 characters.)が表示されていることも確認できています。
##deleteのコントローラーテストを行う
最後にDeleteのテストを行います。
削除に成功するパターンと、失敗するパターンのテストを作成します。
// テストを追加
public function test_delete_member(){
$count = count(Model_Member::find("all"));
$id = 1;
$response = Request::forge('member/delete')
->set_method('POST')
->execute(["id" => $id])
->response();
$updated_user = DB::select()->from("members")->where("id",$id)->execute();
$this->assertEquals(0, count($updated_user));
$this->assertEquals("302",Response::get_redirect_status());
$this->assertEquals($count - 1,count(Model_Member::find("all")));
}
public function test_delete_member_error(){
$count = count(Model_Member::find("all"));
$id = 100;
$response = Request::forge('member/delete')
->set_method('POST')
->execute(["id" => $id])
->response();
$this->assertEquals("302",Response::get_redirect_status());
$this->assertEquals($count, count(Model_Member::find("all")));
}
実行します。
$ oil t group=Controller
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
............
Time: 484 ms, Memory: 18.50Mb
OK (12 tests, 17 assertions)
成功しました。
#通して実行する
コントローラーとモデルのテストを通して実行してみます。
$ oil t
Tests Running...This may take a few moments.
PHPUnit 3.7.37 by Sebastian Bergmann.
Configuration read from /home/unko/sample/fuel/app/phpunit.xml
............................................................... 63 / 387 ( 16%)
............................................................... 126 / 387 ( 32%)
............................................................... 189 / 387 ( 48%)
............................................................... 252 / 387 ( 65%)
............................................................... 315 / 387 ( 81%)
............................................................... 378 / 387 ( 97%)
.........
Time: 674 ms, Memory: 21.25Mb
OK (387 tests, 466 assertions)
テストかくにん! よかった♡
これで、モデルとコントローラーのCRUDテストすべてをPHPUnitのみで実施できる環境が構築できました!
#まとめ
FuelPHPでコントローラーのテストを行う方法がわからなかったのでまとめました。
やり方を調べる過程で、PHPのプロジェクトでJenkins+PhingでCIされている人のブログを結構目にしましたが、コントローラー(ファンクショナル)テストの自動化については情報がなかったように思えます。
StackOverflowで検索したら、それSeleniumでできるよみたいなコメントが結構あって、PHPer爆発したらいいのにと思いましたが、今回まとめた手順によりPHPUnitのみでコントローラーテストまで可能ということがわかりましたので、同様の悩みを持ってる人はぜひお試しください。
また別に違う方法でやっている、やっていたなど情報があればぜひ共有お願いします。
【追記】 FuelPHP 1.8.0.4 以降のテスト
FuelPHP1.9
からcore/classes/request
の中身が刷新され、Request::set_post()
やRequest::set_get()
のセッターメソッドが追加される予定があるというディスカッションがありました。
https://fuelphp.com/forums/discussion/comment/21061
こちら1.9からのはずが、2017/06現在、oil
で最新のFuelPHP
を落としてくるとfuel/core/request
が刷新されており、1.8
からRequest::set_post()
Request::set_get()
が使用できるようになっています。
set_get(), set_post()
set_get(),set_post()によって、あるコントローラーから別のコントローラー/アクションにGET・POSTを付与して呼び出すことが出来ます。
Request->forge('some/url')
->set_method('post')
->set_post('postvar', 'value')
->set_get(array('getvar1' => 'value', 'getvar2' => 'value'))
->execute();
テストへの影響
set_メソッドの登場により、$_POST
, $_GET
に直接値を入れることができなくなります。Request
クラスは内部でInput
クラスを呼び出していますが、こちらも大きく刷新されており、上記の方法ではテストできなくなります。
新しいRequest, Inputクラスを使ったユニットテスト
FuelPHPのfuel/core/request
のバージョンが1.8.0.4
以上の場合、POSTのテストを下記のように記述する必要があります。
class Test_Model_Marea extends TestCase
{
public function test1()
{
$_SERVER['REQUEST_METHOD'] = 'POST'; ←サーバ変数を追加
$r = Request::forge('hoge/create')
->set_method('POST')
->set_post("id","fuga")
->set_post("password","xxxxx")
->execute()
->response();
$this->assertEquals($r->status,200);
}
}
しかしながら現在のバージョンではset_method('POST')
と記述してもGET
が渡ってしまうため、scaffoldで生成したコントローラーのテストが失敗してしまいます。
サーバ変数'REQUEST_METHOD'
を追加することで対応可能となります。
また入力値の初期化メソッドを下記のように書き換えます。
<?php
namespace My;
class InputEX extends \Fuel\Core\Input {
public static function reset(){
parent::$instance = null;
}
}
【追記】
※コメントで指摘いただいた箇所を訂正しました。