Laravel3、結合テスト

タグ: Laravel3   テスト志向  

この記事は、Laravel3でのテストについてのシリーズです。

テストの対象は、以下のシリーズで作成した、りんごテーブルに対する簡単なCRUD操作プログラムに対するものです。

結合テスト

さて、結合テストです。機能テストも入っています。シナリオテストとも言える部分が含まれています。呼び名にはこだわらない主義です。あしからず。

作成したappleコントローラー、Apple Eloqunetとデータベース、リポとバリデータークラスは一つとなり実際には機能します。連携して上手く動作することを確認するテストとなります。

全部細かくテストすると量が膨大になりますので新規作成と1件読み込み表示に対するテストでいくつかのパターンを紹介しておくにとどめます。Laravel3を使用する上でのヒントなどを中心として紹介していきます。

routes.phpの一部

関係する部分のみ示します。

// りんご一件表示
Route::get( 'apple/show/(:num)', array ( 'as' => 'show-apple', 'uses' => 'apple@show' ) );

// りんご追加
//  フォーム表示
Route::get( 'apple/add', array ( 'as' => 'create-apple', 'uses' => 'apple@create' ) );
//  フォーム処理
Route::post( 'apple/add', array ( 'as' => 'create-apple', 'before' => 'csrf', 'uses' => 'apple@create' ) );

// りんご削除
Route::get( 'apple/delete/(:num)', array ( 'as' => 'delete-apple', 'uses' => 'apple@delete' ) );

// りんご更新
Route::get( 'apple/update/(:num)', array ( 'as' => 'update-apple', 'uses' => 'apple@update' ) );
Route::post( 'apple/update/(:num)', array ( 'as' => 'update-apple', 'before' => 'csrf', 'uses' => 'apple@update' ) );

テストコード

<?php

/*
 * 機能テスト(結合テスト)
 *
 * 対象:
 *  Apple Eloquentモデル
 *  AppleRepo
 *  AppleValidator
 *  appleコントローラー
 */

class AppleControllerFunctionallyTest extends \PHPUnit_Framework_TestCase
{
    public static function setUpBeforeClass()
    {
        Schema::create( 'apples',
            function ($table)
            {
                $table->increments( 'id' );
                $table->string( 'name', 10 );
                $table->string( 'color', 10 );
                $table->timestamps();
            } );

        $apple = new Apple( array (
            'name' => '赤りんご',
            'color' => '赤',
            ) );
        $apple->save();

        $apple = new Apple( array (
            'name' => '青りんご',
            'color' => '青',
            ) );
        $apple->save();
    }

    protected function setUp()
    {
        Session::load();

        IoC::register( 'Apple', 'Apple' );
        IoC::register( 'AppleRepo', '\\ProjectH\\Repositories\\AppleRepo' );
        IoC::register( 'AppleValidator', '\\ProjectH\\Services\\Validators\\AppleValidator' );
    }

    public static function tearDownAfterClass()
    {
        Schema::drop( 'apples' );
    }

    public function testGetCreate()
    {
        Request::$foundation->setMethod( 'GET' );
        $response = Controller::call( 'apple@create' );

        $this->assertTrue( $response->foundation->isOk() ); // 200
        $this->assertEquals( $response->status(), '200' ); // 200

        $this->assertEmpty( $response->content->errors->all() ); // バリデーションエラー

        $content = $response->render();

        $this->assertRegExp( '+<label for="name">りんご名</label>+', $content );
        $this->assertRegExp( '+<label for="name">りんご名</label>+', $response->content );
    }

    public function testPostCreateNormalTerminal()
    {
        Request::foundation()->request->add(
            array ( 'name' => '腐ったりんご', 'color' => '茶緑白' )
        );

        Request::$foundation->setMethod( 'POST' );
        $response = Controller::call( 'apple@create' );

        $this->assertTrue( $response->foundation->isRedirection() );

        $apple = DB::table( 'apples' )->where( 'name', '=', '腐ったりんご' )->first();

        $this->assertEquals( '腐ったりんご', $apple->name );
        $this->assertEquals( '茶緑白', $apple->color );
    }

    public function testPostCreateValidationFaildByNotUniqueName()
    {
        Request::foundation()->request->add(
            array ( 'name' => '赤りんご', 'color' => '赤' )
        );

        Request::$foundation->setMethod( 'POST' );
        $response = Controller::call( 'apple@create' );
dd($response);
        $this->assertTrue( $response->foundation->isRedirection() );

        $errors = Session::get( 'errors' );

        $this->assertTrue( $errors->has( 'name' ) );
        $this->assertFalse( $errors->has( 'color' ) );
    }

    public function testPostCreateValidationFaildByEmptyItems()
    {
        Request::foundation()->request->add(
            array ( 'name' => '', 'color' => '' )
        );

        Request::$foundation->setMethod( 'POST' );
        $response = Controller::call( 'apple@create' );

        $this->assertTrue( $response->foundation->isRedirection() );

        $errors = Session::get( 'errors' );

        $this->assertTrue( $errors->has( 'name' ) );
        $this->assertTrue( $errors->has( 'color' ) );
    }

    public function testGetShowNormalTerminal()
    {
        Request::$foundation->setMethod( 'GET' );
        $response = Controller::call( 'apple@show', array ( 1 ) );

        $this->assertTrue( $response->foundation->isOk() ); // 200

        $this->assertEmpty( $response->content->errors->all() ); // バリデーションエラー

        $content = $response->render();

        $this->assertRegExp( '+<p>ID:1</p>+', $content );
        $this->assertRegExp( '+<p>りんご名:赤りんご</p>+', $content );
        $this->assertRegExp( '+<p>りんご色:赤</p>+', $content );
    }

    public function testGetShowNoId()
    {
        Request::$foundation->setMethod( 'GET' );
        $response = Controller::call( 'apple@show', array ( 100000 ) ); // 存在しないID

        $this->assertTrue( $response->foundation->isNotFound() ); // 404
    }

}

「LaravelがSymfonyの上で動作している」と揶揄する方もいらっしゃいますが、その人は2つの誤解を持っています。一つはSymfony2の本質はライブラリーです。それぞれのライブラリーはコンポーネントと呼ばれています。たぶん、Composer上で管理されているからでしょう。

そして、LaravelがSymfonyで動作していることをからかうのは、Symfonyのスタンダードエディションが多くの機能を持ったフレームワークであるがゆえに速度的には早くなく、Laravelがそのフレームワーク上で動いているから遅いフレームワークであると勘違いしているのです。Symfonyスタンダードエディションを始めとして、CMSのDrupalと同様に、LaravelはライブラリーとしてSymfony2のコンポーネントの一部を使用しています。決して、Symfonyスタンダードエディション上で動作しているわけでありません。

ライブラリーとしてのSymfonyは出来が良く、それ故に多くのフレームワークやCMSで使用されています。それだけのことです。速度のことは御心配なく、機能の極端に少ないマイクロフレームワークには負けますが、フルスタックのPHPフレームワークの中では早い方です。

今回のテストコードを見ていただければ、Symfonyを使用するメリットの一部が理解できるでしょう。レスポンスのfoundationプロパティーはSymfonyのライブラリーに含まれるクラスのインスタンスです。その、豊富なメソッドを使用して結果のチェックができます。

さて、テストコードを見てみましょう。

setUpBeforeClassでデータベースの準備を行なっています。テストデーターを2件登録しています。

各テスト前で実行されるsetUpでは、セッションを開始し、IoCでスタブではなく本物のクラスを指定することで、実際のクラスを利用して結合テストを行うことを明示しています。

テスト終了前にtearDownAfterClassでappleテーブルをdropしています。後始末をきちんと行わないと、他のテストに影響が出てしまいます。(大抵の場合、テーブルを作成しようとするコードが「もうテーブルがあるよ」と叱られることになります。)

testGetCreate

最初のテストをじっくり見て行きましょう。りんごレコードを新規で作成するページの表示のテストです。

テスト対象のappleコントローラーはプロパティーの$restfulにtrueを設定し、REST(RESTフル)動作をさせるように指定しています。実行されるメソッド名はHTTPメソッドがアクションの先頭に付く形式となります。

そのため、テストの前にリクエストのHTTPメソッドをシミュレートしてあげる必要があります。

Request::$foundation->setMethod( 'GET' );

デフォルトではGETになるようですが、GETの場合でもどのHTTPメソッドを使用して呼び出しているかを明確にするため、指定しています。

RESTフル動作をさせない場合は、action_が先頭に付き、HTTPメソッドにより実行されるメソッド名は受けません。ですから、この一行はテストコードに入れる必要がありません。

$response = Controller::call( 'apple@create' );

コントローラー名@アクション名でコントローラーのメソッドが呼び出され、結果がLaravel\Responsクラスとして戻ってきます。もし、メソッドに引数を渡す必要があれば第2引数として、配列で渡します。

Laravelでは、アクションフィルターとしてコンストラクターにフィルターを記述できます。この呼び出しではアクションフィルターは動作します。ですから、シンプルにテストしたいのでしたら、アクションフィルターの使用は避け、routes.phpで記述すべきです。逆に、テストは多少複雑になっても、phpunitで一度にテストがしたいのであれば、アクションフィルターを使用する手もあるでしょう。

$this->assertTrue( $response->foundation->isOk() ); // 200
$this->assertEquals( $response->status(), '200' ); // 200

レスポンスコードのチェックです。最初は前記のSymhonyのfoundationクラスのメソッドを利用し、意味が読み取れるようにチェックしています。2番めはLaravelがセットするステータスコードの文字列を比較してチェックしています。お好みでどちらか一つを行えば十分ですね。

$this->assertEmpty( $response->content->errors->all() ); // バリデーションエラー

これはViewクラスのインスタンスをリターンする場合限定で、バリデーションエラーをチェックしています。今回は入力データーがありませんので、もちろんありません。これは、こんなこともできるよと表すために書いただけで、実用性はありません。

ビューに渡すデーターは$response->contentの下の属性として保持されています。

$content = $response->render();

これもViewクラスのインスタンスをリターンした場合のみ限定です。renderメソッドはその名の通り、ビューの内容をレンダーし$respons->contentにセット、およびリターン値として返します。ですから例えば、レンダーした後でerrorsは調べられません。

$this->assertRegExp( '+<label for="name">りんご名</label>+', $content );
$this->assertRegExp( '+<label for="name">りんご名</label>+', $response->content );

ビューの出力内容をチェックしています。$contentも$respons->contentの内容も同じですので、どちらか一方をチェックすればOKでしょう。

testPostCreateNormalTerminal

前記はレコードを新規に追加するフォームの出力でした。今回はそのフォームからのリターンを処理する部分のテストです。

Request::foundation()->request->add(
    array ( 'name' => '腐ったりんご', 'color' => '茶緑白' )
);

フォームの入力内容をシミュレートしています。ここらへんの処理が完成されているので多くのCMSやフレームワークでSymhonyのfoundationクラスを使うんでしょうね。あまり難しいことを考えなくても、コピペで動作してくれます。

$this->assertTrue( $response->foundation->isRedirection() );

リダイレクトされているかをチェックしています。メソッド名そのままです。

$apple = DB::table( 'apples' )->where( 'name', '=', '腐ったりんご' )->first();

新規作成のルートでバリデーションに引っかかりませんので、レコードが作成されるはずです。今回のプロジェクトではEloquentを利用していますので、あえてDBクラスを利用して読み込んでみました。

$this->assertEquals( '腐ったりんご', $apple->name );
$this->assertEquals( '茶緑白', $apple->color );

読み込んだ内容の確認です。指定した内容で書き込まれているかチェックです。

testPostCreateValidationFaildByNotUniqueName

今度はフォーム入力がバリデーションに引っかかる場合のテストです。

Request::foundation()->request->add(
    array ( 'name' => '赤りんご', 'color' => '赤' )
);

nameはapplesテーブルに対してuniqueバリデーションルールを指定しています。重複を許していません。赤りんごの名前は既に作成していますので、この入力はバリデーションで引っかかります。colorにはuniqueを指定していませんので、重複を許しています。

$errors = Session::get( 'errors' );

$this->assertTrue( $errors->has( 'name' ) );
$this->assertFalse( $errors->has( 'color' ) );

バリデーションエラーの場合、with_errorsメソッドにより、バリデーションエラーメッセージがセッションに保存されます。それを取得し、チェックしています。

バリデーションエラーメッセージはLaravel\Messageクラスのインスタンスです。hasメソッドでエラーメッセージの存在をチェックします。名前はuniqueルールのチェックに引っかかり、エラーメッセージが生成されていますので、hasメソッドはtrueを返します。色はバリデーションに引っかかりませんので、hasメソッドはfalseを返します。

メッセージの内容も取得したければgetメソッドにより配列で獲得できます。最初のメッセージだけで良ければfirstメソッドで文字列として獲得できますが、firstでどのエラーメッセージが取れるのかはLaravelのドキュメントとして明記されていません。多分、ルールの記述順にチェックしていき、一番最初に引っかかったものがfirstで取れるのでしょうが、保証されているわけでありません。画面へメッセージを表示するときはfirstは便利ですが、テストの場合はgetで取り、チェックしたほうが良いでしょう。

testPostCreateValidationFaildByEmptyItems

フォームの名前と色、両方が空の場合のテストです。両項目共にrequireバリデーションルールを指定しています。ですから必ず値が入力されていなければなりません。

$this->assertTrue( $errors->has( 'name' ) );
$this->assertTrue( $errors->has( 'color' ) );

両方共空ですので、両方共バリデーションで引っかかり、エラーメッセージが表示されるでしょう。それをチェックしています。

testGetShowNormalTerminal

CRUDのR、読み込みのテストです。IDを指定し該当するレコードを表示します。

$content = $response->render();

$this->assertRegExp( '+<p>ID:1</p>+', $content );
$this->assertRegExp( '+<p>りんご名:赤りんご</p>+', $content );
$this->assertRegExp( '+<p>りんご色:赤</p>+', $content );

テストデータの最初のレコードが表示されることをチェックしています。

testGetShowNoId

存在しないIDを指定された場合のテストです。このテストケースではIDに10000を指定し、存在していないデータとしています。

$this->assertTrue( $response->foundation->isNotFound() ); // 404

存在していないIDが指定された場合、404エラーとして扱います。それをチェックしています。

目的

高速スクラッチ開発を求め、Laravelを使用する方が多いのが現状です。Laravel4は大型案件にも対応できるよう、テスト周りも充実されています。その代わりに、スピード型の開発を目指している方々には多少式が高くなります。

そこで、クラス分割、テストのやり方を示すことで、Laravel 4へ興味を持ってもらえるように、誘導するのが、今回のチュートリアルの目的でした。テストしやすいようにクラスを分割するヒントと、テストの実際を紹介しました。あまりきれいにはまとまりませんでしたね。でも、実際はこの程度レベルのテストが多いでしょう。

そして、例外を取り入れたのはLaravel 4では例外のハンドリングがかなり楽になります。グローバルで例外の取り扱いをかけるようになります。例えば404エラーでしたら、404エラーの例外を投げることで、そのエラーに対しフィルターを書いておくことで処理できます。

ですから、今回のコードもLaravel4ではより短く書けます。

テストコードも短くなるでしょう。例えばメソッドの指定にRequestクラスを使用する手間を欠けていますが、Laravel4では、callメソッドで指定できるようになっています。

また、レンダリングされたビューのチェックも正規表現を使用していますが、イマイチですね。これも、Laravel4ではSymfonyのDOMクローラー(Crawler)を利用してレンダーリング内容をチェックできます。

そのLaravel4ももうすぐ5月にリリースされます。その前に準備段階として、テストに慣れておきましょう。テストを書きやすくするため、コードも考えるようにしましょう。

Laravel4は良く練られていますよ。わくわくしますね。