Laravel3、結合テスト
この記事は、Laravel3でのテストについてのシリーズです。
- その1:単体テストの準備
- その2:例外のテスト
- その3:Eloquentモデルのテスト
- その4:バリデータークラスのテスト
- その5:リポジトリークラスのテスト
- その6:コントローラークラスのテスト
- その7:結合テスト
テストの対象は、以下のシリーズで作成した、りんごテーブルに対する簡単なCRUD操作プログラムに対するものです。
- 参照:Laravel3で活かす例外
- 参照:クラスのディレクトリー構造をひと工夫
- 参照:例外と小さなクラス達
- 参照:小さなクラスで読み込み削除
- 参照:小さなクラスで更新
結合テスト
さて、結合テストです。機能テストも入っています。シナリオテストとも言える部分が含まれています。呼び名にはこだわらない主義です。あしからず。
作成した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は良く練られていますよ。わくわくしますね。