fuelPHPでPHPUnitを使ったユニット・コントローラーテストをするには

fuelPHPでユニット・コントローラーテストしようと思ったら意外に情報がなかったのでまとめました。

#前準備
##PHPUnitのインストール
プロジェクトの`composer.json`のrequireにphpunitを追加します。

```json:composer.json
    "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:fuel/app/config.oil.php
<?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`モデルを見てみます。

```php:fuel/app/classes/model/member.php
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:fuel/app/tests/model/member.php
<?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:fuel/app/migrations/002_rename_field_member_id_to_member_id_in_members.php
<?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`を設定しないでテストを実行します。

```php:fuel/app/tests/model/member.php
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
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`のコントローラーの記述からもそのことがわかります。

```php:fuel/app/classes/controller/member.php

	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でバリデーションの単体テストをするにはどうするか？

バリデーション専用のテストを別途用意する必要があります。

```php:fuel/app/tests/model/member.php
 //テスト追加
  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:fuel/app/tests/controller/member.php
<?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【チュートリアル】-- コントローラーのテストの方法 --](http://qiita.com/ryurock/items/d785336cf6554576fa77)


##コントローラテストのみが使用するパッケージを作成する
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:fuel/packages/classes/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;
        }

        if (\Fuel\Core\Fuel::$env != 'test')
        {
            $response->send(true);
            exit;
        }

        $response->send(true);
    }
}
```

合わせて`bootstrap.php`も修正します。

```php:fuel/packages/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/
```

```php:fuel/app/tests/bootstrap_phpunit.php
// 末尾に追加
Package::load('my');
```

`phpunit.xml`もプロジェクト専用のものを用意します。

```
$ cp fuel/core/phpunit.xml  fuel/app/
```

```php:fuel/app/phpunit.xml
<?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クラスが動いてくれないので、パス解決のエラー表示を無効にしておきます。~~

```php:fuel/core/config/asset.php
	'fail_silently' => true,　← doc_rootが正しく指定されていればこの処理は不要
```

<b><font color="red">phpunit.xmlのdoc_rootが誤っていたため発生してました。doc_rootを正しく`fuel/public`に設定していればこの箇所は不要です。</font></b>

##viewアクションのコントローラーテストを行う

リダイレクトしてもテストが終了しなくなったので、**viewアクションを呼び出し、存在しているIDならば詳細画面を、そうでなければリダイレクトする**テストを行います。

テストファイルを書き換えます。

```php:fuel/app/tests/controller/member.php
  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:fuel/packages/my/classes/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`へリダイレクトするテストを追加します。

```php:fuel/php/tests/controller/member.php

// テストを追加
  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追加の正常系テストを記述します。

```php:fuel/app/tests/controller/member.php
  // 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)
```

正常に終了しました。

今度はパスワードを未入力とし、バリデーションエラーになるテストを実行します。

```php:fuel/app/tests/controller/member.php
  // バリデーションエラーテストを追加
  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:fuel/packages/my/classes/fieldset_ex.php
<?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:fuel/packages/my/bootstrap.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()`を追加します。

```php:fuel/app/tests/controller/member.php
  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クラスは拡張できない](http://fuelphp.jp/docs/1.6/general/extending_core.html)ので、Inputを継承したInputEXクラスを追加します。

```php:fuel/packages/my/classes/input_ex.php
<?php

namespace My;

class InputEX extends \Fuel\Core\Input {
  public static function reset(){
    parent::$input = null;
  }
}
```
`my`パッケージの`bootstrap.php`に`input_ex.php`を追加します。

```php:fuel/packages/my/bootstrap.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()`を追加します。

```php:fuel/app/tests/controller/member.php
  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()`メソッドで処理するように変更します。

```php:fuel/app/tests/controller/member.php

  //setUpメソッドを追加
  protected function setUp(){
    Session::destroy(); //← ついでにセッションの初期化も追加
    Request::reset_request(true); //← ついでにリクエストデータの初期化も追加
    InputEx::reset();
    Fieldset::reset();
  }
```

これで**インサート、バリデーション、リダイレクトのコントローラーテストができる環境**が整いました！

##editのコントローラーテストを行う

続いて既存のデータに対して更新のテストを行います。

既存データの更新テストを行うためには、データベースを既定の状態に設定する必要がありますが、fuelPHPおよびPHPUnitにはそのような機能がありません。

しかしながら[FuelPHP での PHPUnit によるユニットテスト](http://d.hatena.ne.jp/Kenji_s/20111110/1320922825)でフィクスチャ機能を実装されている方がいますので、これを使わせてもらいます。

[FuelPHP での PHPUnit によるユニットテスト](http://d.hatena.ne.jp/Kenji_s/20111110/1320922825)の手順に沿って`dbfixt`タスクを作成します。

タスクを実行し、既存のテーブルからフィクスチャデータを生成します。

```
$ oil r dbfixt:generate members
```

成功すると`fuel/app/tests/fixture`ディレクトリに`テーブル名_fixt.yml`というファイルが生成されます。

```yml:fuel/app/tests/fixture/members_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:fuel/app/tests/controller/member.php
<?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を使用せず、クエリビルダで取得しています。

```php:fuel/app/tests/controller/member.php

  //更新テスト　正常系
  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のテストを行います。

削除に成功するパターンと、失敗するパターンのテストを作成します。

```php:fuel/app/tests/controller/member.php
  // テストを追加
  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を付与して呼び出すことが出来ます。

```php
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のテストを下記のように記述する必要があります。

```php
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:fuel/packages/my/classes/input_ex.php
<?php

namespace My;

class InputEX extends \Fuel\Core\Input {
  public static function reset(){
    parent::$instance = null;
  }
}
```

【追記】
※コメントで指摘いただいた箇所を訂正しました。