はじめに
PHP のテストといえば PHPUnit が有名ですが、xUnit なフレームワーク故に xSpec の民には辛いものがあります。本記事では Rspec や Jest のように、(個人的に)消耗しない PHP のテストと Laravel への導入方法を紹介します。
Kahlan
テストを describe-it で書くために Kahlan を導入します。Kahlan は PHP の BDD テスティングフレームワークです。
インストール
$ composer require --dev kahlan/kahlan
使用方法
spec
ディレクトリにテストコードを配置して、Kahlan を実行します。
<?php
describe('Example', function() {
it('passes if true === true', function() {
expect(true)->toBe(true);
});
});
$ ./vendor/bin/kahlan
実行結果は、次のように表示されます。
_ _
/\ /\__ _| |__ | | __ _ _ __
/ //_/ _` | '_ \| |/ _` | '_ \
/ __ \ (_| | | | | | (_| | | | |
\/ \/\__,_|_| |_|_|\__,_|_| |_|
The PHP Test Framework for Freedom, Truth and Justice.
src directory :
spec directory : /spec
. 1 / 1 (100%)
Expectations : 1 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped
Passed 1 of 1 PASS in 0.820 seconds (using 18MB)
上記のテストコードを
- expect(true)->toBe(true);
+ expect(true)->toBe(false);
のように変更して Kahlan を実行すると、失敗例が確認できます。
_ _
/\ /\__ _| |__ | | __ _ _ __
/ //_/ _` | '_ \| |/ _` | '_ \
/ __ \ (_| | | | | | (_| | | | |
\/ \/\__,_|_| |_|_|\__,_|_| |_|
The PHP Test Framework for Freedom, Truth and Justice.
src directory :
spec directory : /spec
F 1 / 1 (100%)
Example
✖ it passes if true === true
expect->toBe() failed in `./spec/Example.spec.php` line 5
It expect actual to be identical to expected (===).
actual:
(boolean) true
expected:
(boolean) false
Expectations : 1 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped
Passed 0 of 1 FAIL (FAILURE: 1) in 0.823 seconds (using 18MB)
構文
基本的な構文は、他の xSpec ライブラリと同様です。describe
と it
で骨格を築いて、expect
とマッチャで検証します。
例えば、FizzBuzz の結果を返す FizzBuzz::run
のテストは、
describe('FizzBuzz::run', function() {
it('returns 1 when 1', function() {
expect(FizzBuzz::run(1))->toEqual('1');
});
it('returns Fizz when 3', function() {
expect(FizzBuzz::run(3))->toEqual('Fizz');
});
it('returns Buzz when 5', function() {
expect(FizzBuzz::run(5))->toEqual('Buzz');
});
it('returns FizzBuzz when 15', function() {
expect(FizzBuzz::run(15))->toEqual('FizzBuzz');
});
});
のように書けます。
describe
で「テストグループ」を示し、it
で「テスト単位」を示し、expect
で「検証値」を示し、to*
で「検証方法」を示します。to*
に該当するマッチャは Matchers で確認できます。
テストグループのネストや、状態などを示す context
を利用すると、
describe('FizzBuzz', function() {
describe('::run', function(){
context('when 1', function(){
it('returns 1', function() {
expect(FizzBuzz::run(1))->toEqual('1');
});
});
context('when 3', function(){
…
のように書けます。これで、FizzBuzz
に関する ::run
以外のテストも管理しやすくなり、context
で条件が明確になりました。
また、テスト単位の前処理に beforeEach
を利用すると、
describe('FizzBuzz', function() {
describe('::run', function(){
context('when 1', function(){
beforeEach(function() {
$this->result = FizzBuzz::run(1);
});
it('returns 1', function() {
expect($this->result)->toEqual('1');
});
});
context('when 3', function(){
…
のように書けます。context
の条件を beforeEach
で整えておくと、同グループへのテストの追加が楽です。このほか、afterEach
でテスト単位の後処理、beforeAll
や afterAll
でグループの前処理や後処理が書けます。
Laravel + Kahlan
Laravel のプロジェクトに対して、
$ composer require --dev kahlan/kahlan
で Kahlan をインストールして、
$ ./vendor/bin/kahlan
で spec のテストは実行できますが、Laravel 提供のヘルパが使えないため不便です。
例えば、次のような Laravel のレスポンスやファクトリが実行できません。
$response = $this->get('/');
$user = factory(App\User::class)->make();
PHPUnit で使えていた機能を利用するためには、Laravel と Kahlan の連携が必要です。
連携方針
Laravel のテストを PHPUnit で書く際には tests/TestCase.php
を継承しますが、これは PHPUnit\Framework\TestCase
のサブクラスです。Laravel は、PHPUnit\Framework\TestCase
にメソッドを生やすことで、get()
などのヘルパを提供をしています。そして、Laravel 向けに整えたクラスが Illuminate\Foundation\Testing\TestCase
なので、これを Kahlan から利用すれば get()
などが使えます。
連携のためにはあと一点、tests/CreatesApplication.php
の処理が必要です。これは Laravel アプリケーションである Illuminate\Foundation\Application
を稼働させているため、bootstrap/app.php
を利用して、
$app = require 'bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
を Kahlan と共に実行すれば解決します。
laravel-kahlan4
連携には laravel-kahlan4 を使用します。laravel-kahlan にインスパイアされて作成した、Laravel のテストを Kahlan で書くためのライブラリです。基のライブラリが動作せず放置状態だったため、Kahlan 4 向けに書くと共に機能を拡充しました。
midnightSuyama/laravel-kahlan4
インストール
$ composer require --dev midnightsuyama/laravel-kahlan4
使用方法
Kahlan は実行時に kahlan-config.php を読み込むため、
<?php
LaravelKahlan4\Config::bootstrap($this);
をプロジェクトルートに用意して、Kahlan を実行するだけです。環境変数は、PHPUnit と同様に .env.testing
を優先します。
Laravel テストメソッド
Illuminate\Foundation\Testing\TestCase
ベースのインスタンスを $this->laravel
に格納しているため、Illuminate\Foundation\Testing\Concerns
のテストメソッドが呼べます。
$response = $this->get('/');
のような PHPUnit は、Kahlan で、
$response = $this->laravel->get('/');
のように書けます。
Laravel テストトレイト
beforeAll
で $this->laravel->useTrait()
を呼ぶことで、特定のトレイトを使用できます。
use Illuminate\Foundation\Testing\RefreshDatabase;
describe('User table', function() {
beforeAll(function() {
$this->laravel->useTrait(RefreshDatabase::class);
});
it('has no records', function() {
$count = App\User::count();
expect($count)->toEqual(0);
});
});
Laravel のテストトレイトは、次の 6 種類です。
- Illuminate\Foundation\Testing\RefreshDatabase
- Illuminate\Foundation\Testing\DatabaseMigrations
- Illuminate\Foundation\Testing\DatabaseTransactions
- Illuminate\Foundation\Testing\WithoutMiddleware
- Illuminate\Foundation\Testing\WithoutEvents
- Illuminate\Foundation\Testing\WithFaker
Laravel アサーション対応 マッチャ
Laravel のアサーションに対応したマッチャを用意しています。命名規則は toPass*
と assert*
で対にしているため、改めて覚える必要はありません。
$response->assertStatus(200);
のような PHPUnit は、Kahlan で、
expect($response)->toPassStatus(200);
のように書けます。
レスポンス
describe('Response', function() {
it('has a 200 status code', function() {
$response = $this->laravel->get('/');
expect($response)->toPassStatus(200);
});
});
マッチャ | 説明 |
---|---|
toPassSuccessful() | レスポンスが successful ステータスコードを持っている。 |
toPassOk() | レスポンスが 200 ステータスコードを持っている。 |
toPassNotFound() | レスポンスが not found ステータスコードを持っている。 |
toPassForbidden() | レスポンスが forbidden ステータスコードを持っている。 |
toPassUnauthorized() | レスポンスが Unauthorized ステータスコードを持っている。 |
toPassStatus($status) | レスポンスが指定したステータスコードを持っている。 |
toPassRedirect($uri = null) | レスポンスが指定した URI へリダイレクトする。 |
toPassHeader($headerName, $value = null) | レスポンスに指定したヘッダが含まれている。 |
toPassHeaderMissing($headerName) | レスポンスに指定したヘッダが含まれていない。 |
toPassLocation($uri) | Location ヘッダと指定した URI が一致している。 |
toPassPlainCookie($cookieName, $value = null) | レスポンスに指定したクッキーが含まれている。 |
toPassCookie($cookieName, $value = null, $encrypted = true, $unserialize = false) | レスポンスに指定したクッキーが含まれている。 |
toPassCookieExpired($cookieName) | レスポンスに指定したクッキーが含まれており、期限切れである。 |
toPassCookieNotExpired($cookieName) | レスポンスに指定したクッキーが含まれており、期限切れでない。 |
toPassCookieMissing($cookieName) | レスポンスに指定したクッキーが含まれてない。 |
toPassSee($value) | 指定した文字列がレスポンスに含まれている。 |
toPassSeeInOrder(array $values) | 指定した文字列が順番通りにレスポンスに含まれている。 |
toPassSeeText($value) | 指定した文字列がレスポンステキストに含まれている。 |
toPassSeeTextInOrder(array $values) | 指定した文字列が順番通りにレンスポンステキストに含まれている。 |
toPassDontSee($value) | 指定した文字列がレスポンスに含まれていない。 |
toPassDontSeeText($value) | 指定した文字列がレスポンステキストに含まれていない。 |
toPassJson(array $data, $strict = false) | レスポンスに指定した JSON データが含まれている。 |
toPassExactJson(array $data) | レスポンスに指定した JSON データと完全一致するデータが含まれている。 |
toPassJsonFragment(array $data) | レスポンスに指定した JSON の一部が含まれている。 |
toPassJsonMissing(array $data, $exact = false) | レスポンスに指定した JSON の一部が含まれていない。 |
toPassJsonMissingExact(array $data) | レスポンスに指定した JSON データと完全一致するデータが含まれていない。 |
toPassJsonStructure(array $structure = null, $responseData = null) | レスポンスが指定した JSON の構造を持っている。 |
toPassJsonCount(int $count, $key = null) | レスポンス JSON が指定したキーのアイテムを期待数持っている。 |
toPassJsonValidationErrors($errors) | レスポンスが指定した JSON バリデーションエラーを持っている。 |
toPassJsonMissingValidationErrors($keys = null) | レスポンスが指定したキーの JSON バリデーションエラーを持っていない。 |
toPassViewIs($value) | レスポンスビューと指定したビューが一致する。 |
toPassViewHas($key, $value = null) | レスポンスビューが指定したデータを持っている。 |
toPassViewHasAll(array $bindings) | レスポンスビューが指定したリストのデータを持っている。 |
toPassViewMissing($key) | レスポンスビューが指定したデータを持っていない。 |
toPassSessionHas($key, $value = null) | セッションが指定したデータを持っている。 |
toPassSessionHasAll(array $bindings) | セッションが指定したリストの値を持っている。 |
toPassSessionHasInput($key, $value = null) | セッションが指定した値をフラッシュデータが持っている。 |
toPassSessionHasErrors($keys = [], $format = null, $errorBag = 'default') | セッションが指定したエラーを持っている。 |
toPassSessionDoesntHaveErrors($keys = [], $format = null, $errorBag = 'default') | セッションが指定したエラーを持っていない。 |
toPassSessionHasNoErrors() | セッションがエラーを持っていない。 |
toPassSessionHasErrorsIn($errorBag, $keys = [], $format = null) | セッションが指定したエラーを持っている。 |
toPassSessionMissing($key) | セッションが指定したキーを持っていない。 |
認証
describe('User', function() {
it('is authenticated', function() {
$user = factory(App\User::class)->make();
$this->laravel->actingAs($user);
expect($this->laravel)->toPassAuthenticated();
});
});
マッチャ | 説明 |
---|---|
toPassAuthenticated($guard = null) | ユーザが認証されている。 |
toPassGuest($guard = null) | ユーザが認証されていない。 |
toPassAuthenticatedAs($user, $guard = null) | 指定したユーザが認証されている。 |
toPassCredentials(array $credentials, $guard = null) | 指定した認証情報が有効である。 |
toPassInvalidCredentials(array $credentials, $guard = null) | 指定した認証情報が無効である。 |
データベース
describe('User table', function() {
it('has records', function() {
$user = factory(App\User::class)->create();
expect($this->laravel)->toPassDatabaseHas('users', ['email' => $user['email']]);
});
});
マッチャ | 説明 |
---|---|
toPassDatabaseHas($table, array $data, $connection = null) | 指定したデータがデータベースに存在する。 |
toPassDatabaseMissing($table, array $data, $connection = null) | 指定したデータがデータベースに存在しない。 |
toPassSoftDeleted($table, array $data = [], $connection = null) | 指定したレコードが論理削除されている。 |