7
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

Laravel のテストを describe-it でいい感じに書く

はじめに

PHP のテストといえば PHPUnit が有名ですが、xUnit なフレームワーク故に xSpec の民には辛いものがあります。本記事では RspecJest のように、(個人的に)消耗しない PHP のテストと Laravel への導入方法を紹介します。

Kahlan

logo.png

テストを describe-it で書くために Kahlan を導入します。Kahlan は PHP の BDD テスティングフレームワークです。

kahlan/kahlan

インストール

$ composer require --dev kahlan/kahlan

使用方法

spec ディレクトリにテストコードを配置して、Kahlan を実行します。

spec/Example.spec.php
<?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 ライブラリと同様です。describeit で骨格を築いて、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 でテスト単位の後処理、beforeAllafterAll でグループの前処理や後処理が書けます。

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 を読み込むため、

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) 指定したレコードが論理削除されている。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
7
Help us understand the problem. What are the problem?