以前にもLaravelでのテストについては書いたのですが、5.1以降でテストがより強化(Integrated Testと呼ぶことにする)、簡素化されたので試してみたいと思います。
5.1で何が変わったか?
例えば、以前、JsonAPIのテストを書きたい時は、
public function testJson()
{
//リクエスト+レスポンス
$response = $this->call('GET', '/users/json');
//オブジェクト取得
$obj = $response->getData();
//アサーション
$this->assertEquals("OK",$obj->status);
}
という感じで、PHPUnitのassertを使って、少々冗長でした。
これが、新しく、
public function testJson()
{
//チェック
$this->get('/users/json')
->seeJson([
'status'=>'OK',
]);
}
こんな感じでチェーンメソッドを使いより直感的に、完結に書けるように(も)なりました。
詳しくは、Laravelの本家サイトか、こちらのサイトが参考になります。
注意点
Integrated Testは、非常にお手軽ですが、いくつかの制限があります。
特にクライアント側で実行されるJavaScriptを利用したサイトは基本的にテストできません。Seleniumと連携させる方法もありますが、少し完成度が低いようです。
今のところ向いているのは
- Laravelのお作法に沿った画面のテスト
- WebAPIのテスト
逆に向いていないのは、
- JSを使ったページ
JSを多く使ったページはSelenumを利用する方がいいでしょう。
テストへの最適化
本末転倒感もありますが、Integrated Testを有効に活用するためには、テストしやすいようにコーディングする必要があります。主な留意点としては下記のようなものがあります。
- 入力はFormとSubmitで行う。
- その他のボタンはで実現する(bootstrap使っていれば問題ないかと)。
- javascriptは極力利用しない。
- Form-SubmitやにJSを仕込んだ場合、JSは無視され、action先、href先には移動するので、正常系のテストはできます。
まあTFの考えからすれば、テストできる仕様で実装すべきということかもしれん。ただ、管理画面ならJS無しも不可能ではないですが、フロントは無理ですよね。
Laravelでのテスト実行と書き方
Laravelでは、testsディレクトリ以下にテストを書くルールになっています。
LARAVEL_HOME/phpinit.xmlで定義されているので変更することもできます。
とりあえず実行
既に、tests以下にはExampleTest.phpというサンプルがあるので、とりあえず、それを実行してみます。
前提でLaraelが正常にインストールされ、php artisan serve等で起動していることが前提です。
cd LARAVEL_HOME(お好みで)
./vendor/bin/phpunit
とすると、tests以下の対象ファイルが実行されます。
標準では、ExampleTest.phpの中で、1つのテストと2つのアサート(評価)が行われているので、
OK (1 test, 2 assertions)
とグリーンで表示されるはずです。
テストの対象
Laravel(というかPHPUnit)では、
- xxxTest.phpというファイル(クラス)名
- public function testXXXX()というファンクション名
のいずれかをテスト対象とします(他にも@testとコメントに入れるというものある)。
記述方法
では、ExampleTest.phpの中身を見てみます。
class ExampleTest extends TestCase
{
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
$this->visit('/')
->see('Laravel 5');
}
}
- TestクラスはTestCaseクラスを継承します。
- LaravelのHomeに訪問し、Laravel 5という文字列が存在するかを確認しています。
簡単ですね。
テスト用のサイト(画面)を用意する
長いので、必要ない人は下の「テストをする」を見て下さい。
テスト用に下記2つを用意してみます。
- 簡単なForm(バリデーション付き)と簡単な表示
- Databaseのテスト
- 簡単なJSON
準備
DBも使いたいので、.envを設定して下さい。
で、migrateで生成できるusersテーブルをテスト用に利用してみたいと思います。
php artisan migrate
Form
usersのnameとemailにインサートする簡単なFormを作ってみます。
挿入後はリスト表示してみます。
Route
ひとまずRouteは
Route::get('users/create','UsersController@create'); //フォーム表示
Route::post('users/store','UsersController@store'); //Insert処理
Route::get('users','UsersController@index'); //登録情報リスト表示
の3つを用意しておきます。
Controller
処理を書くControllerを用意します。
php artisan make:controller UsersController
処理を書いて行きます。
保存の際は、nameにだけ、requiredバリデーションを適用しています。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\User;
class UsersController extends Controller
{
//一覧表示
public function index()
{
//create query
$query = User::query();
//get all data
$users = $query->get();
//return view
return view('users.index')->with('users',$users);
}
//Form表示
public function create()
{
//return view(form)
return view('users.create');
}
//保存処理
public function store(Request $request)
{
//validation
$inputs = $request->all();
//rules
$rules = [
'name'=>'required',
];
//message
$messages = [
'name.required'=>'名前は必須です。',
];
//バリデート
$validation = \Validator::make($inputs,$rules,$messages);
//バリデーションエラーだったら、元のページに戻る
if($validation->fails())
{
return redirect()->back()->withErrors($validation->errors())->withInput();
}
//create User
$user = User::create();
$user->name = $request->name;
$user->email = $request->email;
//save
$user->save();
//redirect to list
return redirect()->to('/users');
}
}
View
では、表示用のViewを用意します。resources/view以下にusersフォルダを作成し、以下のファイルを置いていきます。
共通View
テストなので各ファイルに書いてもいいですが、いちおう用意します。
layout.blade.php
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
<body>
<div class="container">
@yield('content')
</div>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
入力フォーム
次に入力フォーム。create.blade.php
@extends('layout')
@section('content')
<h1>新規登録</h1>
<form method="post" action="/users/store">
<div class="form-group">
<label>名前</label>
<input type="text" name="name" value="" class="form-control">
<span class="help-block">{{$errors->first('name')}}</span>
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="text" name="email" value="" class="form-control">
</div>
<input type="hidden" name="_token" value="{{csrf_token()}}">
<input type="submit" value="登録" class="btn btn-primary">
</form>
@stop
一覧表示
@extends('layout')
@section('content')
<h1>一覧表示</h1>
<div class="row">
<div class="col-sm-12">
<a href="/users/create" class="btn btn-primary" style="margin:20px">新規登録</a>
</div>
</div>
<table class="table table-striped">
@foreach($users as $user)
<tr>
<td>{{$user->id}}</td>
<td>{{$user->name}}</td>
<td>{{$user->email}}</td>
</tr>
@endforeach
</table>
@stop
以上です。
テストをする
新しいクラスを用意してもいいですが、ここでは、ExampleTest.phpを編集することにします。
Formをテストする
どうやって、入力してSubmitボタンを押すのか私も気になりましたが、非常に簡単です。
なお、Userのemailには標準でunique属性が付いているので、テストの毎に違うemailが入るようにFakerを利用してemailを自動生成しています。
やっていることは、
- user/createにアクセス(できるか?)
- 新規登録とう文字列が含まれているか?
- nameにHogeと入力
- emailにFakerがユニークに生成したemailを挿入
- Submitボタンを押す(valueで指定)
- 移動したページに一覧表示が含まれるか?
- データベースに挿入したemailが存在するか?
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
public function testCreateOK()
{
//emailを生成
$faker = Faker\Factory::create('ja_JP');
$email = $faker->unique()->email;
$this->visit('users/create')
->see('新規登録')
->type('Hoge','name')
->type($email,'email')
->press('登録')
->see('一覧表示');
//check db
$this->seeInDatabase('users',['email'=>$email]);
}
}
typeだけでなく、radioボタン、selectはselectを。checkboxはcheckを利用するようです。
また、seeInDatabaseで挿入された値もワンラインで確認できます。便利!。
テストを実行してみる
テストは、
./vendor/bin/phpunit
で、OKです。
バリデーションエラーの場合
バリデーションエラーの場合もチェックしてみたいと思います(functionのみ書きます)。
public function testCreateNG()
{
$this->visit('users/create')
->see('新規登録')
->type('','name')
->type('foo@hoge.com','email')
->press('登録')
->see('名前は必須です。');
}
nameを空で送信してみます。
メッセージで「名前は必須です。」が出ているはずなので、それが出ているかチェックしています。
Formはあとはいろいろと工夫すれば済むようです。
JSON APIをテストする
次にJSON APIをテストしてみたいと思います。
DBに溜まった情報を、単純にJSONで出力してみます。いちおう、ステータスも変えまします。
Controller
コントローラーにメソッドを追加します。
public function indexJson()
{
$query = User::query();
$users = $query->get();
$response['status'] = "OK";
$response['data'] = $users;
return \Response::json($response);
}
テストコード
public function testJson()
{
$this->get('/users/json?key=kagi')
->seeJson([
'status'=>'OK',
]);
}
便利。
おまけ(調査中)
あまりに便利なので、Laravel以外のテストにも使えればと思い少し調べましたが、結論NGでした。
元クラスで$baseUrlが定義されているので、変更してみたましたが、どうしても、ローカルを見てるみたいです。
$this->baseUrl = "http://www.yahoo.co.jp";
$this->visit("/")->see("Yahoo");
としてみましたが、アクセスできませんでした。
Laravelではない、普通の環境でlaracast/integratedを利用した場合はOKだったので、Laravelへの統合クラスのバグかもしれません。
LaravelTestCase.phpの中で、baseUrlが"http://localhost/"でスタティックにreturnされているからみたいです。
とりあえず素のlaracast/integratedなら動くのでこちらも参考にしてみてください。