読者です 読者をやめる 読者になる 読者になる

tanaka's Programming Memo

プログラミングについてのメモ。

Laravel5.3でのテストのメモ

自分向けのメモです。

LaravelにはPHPUnitによるテストの設定が組み込まれています。公式マニュアルで概要を確認して、データベースのチェックや、URLごとのテストについて調べました。

5.2から5.3になる段階で、データベースとモックに関する記述が増えていました。


実行時の注意点

以下のようなエラーが表示された場合の対処方法です。

PHP Fatal error:  Call to undefined method PHPUnit_Framework_TestResult::warnings() in /Users/yutanaka/.composer/vendor/phpunit/phpunit/src/TextUI/ResultPrinter.php on line 297

Laravelに組み込まれているphpunitはやや古い可能性があります。グローバルのphpunitとバージョンが異なる場合に、上記のようなエラーが表示される場合があります。その際は、Laravelに組み込まれているphpunitを呼び出すように package.json に以下のようなスクリプト呼び出しを追加して、 npm test でテストを開始すると良いでしょう。

  "scripts": {
    // :
    "test": "vendor/phpunit/phpunit/phpunit"
  },

公式マニュアル概要

Testing - Laravel - The PHP Framework For Web Artisans を参照して、概要を確認します。

Introduction

  • Laravelアプリフォルダー直下のphpunit.xmlと、testsフォルダー内にサンプルが設定されている
  • アプリフォルダー内で phpunit で実行できる
テスト環境
  • テストを開始すると、Laravelが自動的に環境設定をtestingに設定する
  • Laravelは、テスト中はarrayドライバーでセッションやキャッシュを設定するので、テスト中のセッションやキャッシュは後に残ることがない
  • テスト環境は、phpunit.xmlファイルのtesting環境変数で変更して構わない。変更した後は、artisanコマンドのconfig:clearを実行して、設定のキャッシュをクリアすること
新しいテストの定義と実行
  • php artisan make:test UserTest などで、新しいテストを定義できる
  • testsフォルダー内に、新しいUserTestファイルが作成されるので、phpunitのコマンドを書いてテスト内容を記述する
  • 実行は、phpunitを呼び出す
  • setUpメソッドをオーバーライドする時は、setUp関数内で parent::setUp() として、親のsetUpを呼び出すこと

アプリケーションのテスト

Application Testing - Laravel - The PHP Framework For Web Artisans

  • Laravelは、HTTPリクエストを生成したり、出力をテストしたり、フォームを埋めたりするのが楽になるAPIを提供する
  • visit()メソッドは、GETリクエストをアプリケーションに発行する
  • see()メソッドは、アプリケーションの戻り値から指定した文字列が含まれるかをアサートする
  • dontSee()メソッドは、指定の文字列が戻り値に含まれないことをアサートする
  • visitRoute()メソッドは、ルート名でGETリクエストを生成できる
$this->visitRoute('profile');

$this->visitRoute('profile', ['user' => 1]);

アプリケーションとの連携

リンクをクリックする
  • visit()で画面を表示
  • click('<クリックしたい文字列>')
  • seePageIs('遷移先のURL')
<a href="/about-us">About Us</a>
  • 上記のHTMLがあった場合、以下のテストが使える
public function testBasicExample()
{
    $this->visit('/')
         ->click('About Us')
         ->seePageIs('/about-us');
}
  • seePageIs()の代わりに、 seeRouteIs() も使える
->seeRouteIs('profile', ['user' => 1]);
フォームの操作
  • type(<入力内容>, <フォームのname>)で、指定のnameの入力フォームに指定のテキストを入力する
  • select(<選択内容>, <フォームのname>)で、ラジオボタンやドロップダウンの選択
  • check(<フォームのname>)で、指定のチェックボックスにチェック
  • uncheck(<フォームのname>)で、指定のチェックボックスのチェックを外す
  • attach(<ファイルのパス>, <フォームのname>)で、指定のファイルをアップロード対象にする
public function testPhotoCanBeUploaded()
{
    $this->visit('/upload')
         ->attach($pathToFile, 'photo')
         ->press('Upload')
         ->see('Upload Successful!');
}
  • press(<ボタンやテキストや要素のname>)で、該当するものを押す
JSONテスト
  • json, get, post, put, patch, deleteメソッドで、指定のURLに、指定のパラメータをJSONで渡した呼び出しができる
  • seeJson()で、指定のデータが戻り値に含まれるかをチェックする。「含むか」なので、完全一致じゃなくてもテストは成功する
  • JSONの完全な一致をチェックしたい場合は、seeJsonEquals()メソッドを利用する
  • 戻り値の内容ではなく、構造をチェックしたい場合は、seeJsonStructure()メソッドを使う。指定していないキーがあっても、指定した構造が含まれていればテストは成功する
  • 何らかのキーに、指定の構造が含まれるかをチェックする場合は、 * を使う。ネストも可能
SessionとAuthentication
  • withSession()メソッドで、セッションを設定できる。ページを訪れる前に、テストしたい値をセッションに設定しておくことができる
  • actingAs()で、カレントユーザーを設定できる。事前に、ModelFactoryで、新規のユーザーモデルを作成して、それを引数に渡してユーザーを作成できる。Factoryでテスト用ユーザーを登録して、それをユーザーとして認証する例
<?php

class ExampleTest extends TestCase
{
    public function testApplication()
    {
        $user = factory(App\User::class)->create();

        $this->actingAs($user)
             ->withSession(['foo' => 'bar'])
             ->visit('/')
             ->see('Hello, '.$user->name);
    }
}
  • actingAs()の第2引数を指定すると、毎回認証が必要な保護認証に対して、保護名を設定できる
ミドルウェアの無効化
  • WithoutMiddlewareトレイトを使うと、ミドルウェアを無効化して、テストを簡易にできる
    • クラス内に、use WithoutMiddleware;を宣言すると、そのテスト全てでミドルウェアが無効になる
<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use WithoutMiddleware;

    //
}
    • 特定のテストのみで無効にしたい場合は、テストメソッドの中で、$this->withoutMiddleware();を呼び出す
<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->withoutMiddleware();

        $this->visit('/')
             ->see('Laravel 5');
    }
}
カスタムHTTPリクエスト
  • カスタムリクエストを作成して、戻り値のIlluminate\Http\Responseオブジェクトを取得したい場合は、call()メソッドを使う
public function testApplication()
{
    $response = $this->call('GET', '/');

    $this->assertEquals(200, $response->status());
}
  • POST, PUT, PATCHリクエストに必要な入力データは、配列で渡す
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
PHPUnitアサーション

以下、Laravelが提供するPHPUnitテスト用のメソッド。

  • assertResponseOk();
    • クライアントのレスポンスコードがOkかを判定
  • assertResponseStatus($code);
  • assertViewHas($key, $value = null);
    • 戻り値のビューに対して、$keyに$valueが設定されているかを判定
  • assertViewHasAll(array);
    • 戻り値が、指定の配列を持つかを判定
  • assertViewMissing($key);
    • 戻り値のビューに指定のkeyが含まれないことを判定
  • assertRedirectedTo($uri, $with = []);
    • 指定のURIにリダイレクトしたかを判定
  • assertRedirectedToRoute($name, $parameters = [], $with = []);
    • 指定のルートにリダイレクトされたかを判定
  • assertRedirectedToAction($name, $parameters = [], $with = []);
    • 指定のアクションにリダイレクトされたかを判定
  • assertSessionHas($key, $value = null);
    • セッションが指定のkeyとvalueを持つかを判定
  • assertSessionHasAll(array $bindings);
    • セッションが指定の配列の値を持つかを判定
  • assertSessionHasErrors($bindings = [], $format = null);
    • セッションが指定のエラーになっていないかを判定
  • assertHasOldInput();
    • セッションがold inputを持っていないかを判定
  • assertSessionMissing($key);
    • セッションが指定のkeyを持っていないことを判定

Databaseのテスト

Laravelは、データベース駆動のアプリケーション向けのテスト環境も提供しています。

  • seeInDatabase()メソッドで、指定のデータが、データベースの指定のテーブルに含まれるかを判定

テスト後のデータベースのリセット

各テスト後に、テスト時のデータベースへの変更をもとに戻す方法です。

Migrationを使う
  • 次のテストの前に、DatabaseMigrationsトレイトを使って、Migrationする
  • テストクラス内で、 use DatabaseMigrations; を定義すれば、テストごとに自動的にマイグレーションを実施する
トランザクションを使う
  • 全てのテストを、データベースのトランザクションで囲む方法もある
  • DatabaseTransactionsトレイトを使えば、Migrationsと同様にテストごとに自動的にこれを行う
  • このトレイトは、デフォルトのデータベース接続にのみ対応

モデルファクトリー

複数のテストにまたがって、共通の幾つかのレコードをデータベースに登録したい場合、手動で特定の値を列ごとに設定するのではなく、Eloquentのモデルファクトリーを利用することができます。

  • database/factories/ModelFactory.php ファイルに、データの定義例がある
  • $factory->define()メソッドに、デフォルトデータを戻すクロージャーを渡す
  • クロージャーは、Faker PHP ライブラリのインスタンスを返し、テストのための乱数を設定できる
  • ModelFactory.phpファイルには、自由にファクトリーを追加できる
  • UserFactory.phpやCommentFactory.phpなど、database/factoriesディレクトリーに加えることができる
ファクトリーの States
  • Statesとして、個別の修正を定義して、様々な組み合わせでモデルファクトリーに適用できる
  • Userモデルに delinquent というstate を持たせて、デフォルトの属性値を設定するなどができる
  • account_status属性に'delinquent'という値を持たせるためのステータスである delinquent を宣言する例
$factory->state(App\User::class, 'delinquent', function ($faker) {
    return [
        'account_status' => 'delinquent',
    ];
});

Factoryの利用

モデルの作成
  • ファクトリーを定義したら、テスト関数でfactory(モデルクラス)->make();とすれば、ファクトリーから生成したモデルのインスタンスを得られる
public function testDatabase()
{
    $user = factory(App\User::class)->make();

    // Use model in tests...
}
  • factory()の第2引数に数を渡すと、指定の数のインスタンスを生成する
  • 同じく、ファクトリー名を渡すと、該当するファクトリーを生成する
// Create three App\User instances...
$users = factory(App\User::class, 3)->make();

// Create an "admin" App\User instance...
$user = factory(App\User::class, 'admin')->make();

// Create three "admin" App\User instances...
$users = factory(App\User::class, 'admin', 3)->make();
States を適用する
  • モデルに state を適用することができる
$users = factory(App\User::class, 5)->states('deliquent')->make();
  • 多数の state をモデルに適用するには、その名前を以下のように指定する
$users = factory(App\User::class, 5)->states('premium', 'deliquent')->make();
属性の上書き
  • モデルのいくつかのデフォルト値をオーバーライドしたい場合、makeメソッドに配列を渡す
  • 指定した値のみが上書きされて、それ以外の値はファクトリーに設定されているデフォルト値が設定される
$user = factory(App\User::class)->make([
    'name' => 'Abigail',
]);
モデルを生成して保存する
  • create メソッドは、モデルインスタンスを作成した上で、Eloquentの save メソッドで保存する
    // Create a single App\User instance...
    $user = factory(App\User::class)->create();

    // Create three App\User instances...
    $users = factory(App\User::class, 3)->create();
  • makeメソッドと同様に配列を渡すと値を設定することができる
リレーションしたデータの作成
  • create()してモデルを作成すると collection のインスタンスが返されるので、 each() メソッドなどを使って作成されたばかりのモデルのインスタンスを取り出して、リレーションさせる他のモデルを生成することができる
$users = factory(App\User::class, 3)
           ->create()
           ->each(function ($u) {
                $u->posts()->save(factory(App\Post::class)->make());
            });
リレーションと属性のクロージャ
  • ファクトリーの定義で、モデルにリレーションを設定するのにクロージャー属性を使うこともできる
  • 新しい Post を作成するのと同時に、新しい User のインスタンを作成する方法は以下の通り
$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});
  • これらのクロージャーは、引数として、そのクロージャーを含むファクトリーの属性の配列を受け取る
    • Postを生成した時に User も作成
    • user_type のクロージャーは $post 配列を受け取って、そこから user_id で生成した User の IDを取り出して検索して、Userに設定されているtypeと、Postのuser_typeをリレーションさせている
$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        },
        'user_type' => function (array $post) {
            return App\User::find($post['user_id'])->type;
        }
    ];
});

モック

イベントのモック

  • Laravelで大量にイベントシステムを構築していた場合、テスト中にイベントをモック化できる
  • ユーザー登録をした時に、登録が完了したイベント(例えばUserRegistered)が発行すると、登録完了のメールが送信されてりする。それをキャンセルするとテストが楽である
  • $this->expectsEvents(App\Events\UserRegistered::class); とすると、UserRegisteredイベントをキャンセルできる
  • doesntExpectEvents()メソッドを使うと、指定のイベントが発動しなかったことを確認できる
  • withoutEvents()メソッドを呼び出すと、すべてのイベントハンドラを抑制できる

Fakeの利用

  • Eventファサードの fake メソッドを利用することでもモック化をして、すべてのイベントリスナーを無効にできる
  • それから、イベントが発生したかや、どのようなデータを返したかをチェックすれば良い
<?php

use App\Events\OrderShipped;
use App\Events\OrderFailedToShip;
use Illuminate\Support\Facades\Event;

class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function testOrderShipping()
    {
        Event::fake();

        // Perform order shipping...

        Event::assertFired(OrderShipped::class, function ($e) use ($order) {
            return $e->order->id === $order->id;
        });

        Event::assertNotFired(OrderFailedToShip::class);
    }
}

ジョブのモック

アプリケーションがリクエストを作成した時に、作成したコントローラーが特定のジョブを呼び出すかを試す簡単なテストをしたい場合があるでしょう。そのような時の方法です。これにより、ルートとコントローラーのテストを分離することができます。

  • $this->expectsJobs(App\Jobs\PurchasePodcast::class); などのようにあらかじめ呼び出しておくことで、指定のジョブが呼び出されたことを確認する。行うのは確認のみで、ジョブ自体を実行することはしない
  • このメソッドは、DispatchesJobsトレイトか、dispatch()メソッドによって発行されたジョブにのみ反応する。Queue::push()で直に送信されたジョブは対象外
  • ジョブが呼び出されなかったことを確認するには、 $this->doesntExpectJobs(ShipOrder::class) などのようにする
  • $this->withoutJobs() をテストメソッド内で呼び出すと、そのテスト中に予約されたすべてのジョブは破棄される
Fakeの利用
  • Queueファサードの fake メソッドで、キューに積まれたジョブをモック化できる
  • その後、キューにジョブが積まれたかを確認したり、それらが受け取るデータを検査すれば良い
  • アサーションは、fakeを呼び出したあと実行する

メールのFake

  • Mailファサードの fake メソッドを使うと、メールの送信を抑制できる
  • その後、ユーザーに送られるはずの mailables をアサーションしたり、受け取ったデータを検査する
<?php

use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Mail::fake();

        // Perform order shipping...

        Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
            return $mail->order->id === $order->id;
        });

        // Assert a message was sent to the given users...
        Mail::assertSentTo([$user], OrderShipped::class);

        // Assert a mailable was not sent...
        Mail::assertNotSent(AnotherMailable::class);
    }
}

NotificationのFake

  • Notificationのfakeも使い方は同様

Mocking - Laravel - The PHP Framework For Web Artisans

Facadeのモック

Facade(ファサード)は、Laravelのサービスを静的に呼び出せるようにしたもの。これをモックにします。例えば、Cacheファサードをモックに入れ替えてテストができます。

  • shouldReceive()メソッドを呼び出すと、Mockeryクラスのモックのインスタンスが返される
  • Laravelのservice containerにより管理されるので、そのままクラスを利用するよりもテストしやすい
<?php

class FooTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $this->visit('/users')->see('value');
    }
}
  • Requestファサードをモックにすると、テストを実行する時にcall()やpost()メソッドのようなHTTPヘルパーメソッドもキャンセルされてしまうので、モックにするべきではない