公開日: 2020-05-27
更新日: 2020-05-27

Laravelでphpunitの実行時に、 fakerを呼び出したり、データベースに登録されているデータを参照したりする方法

Laravelのphpunitで遅延評価を使う

App\Http\Requests\UserRequest.phpをテストするクラスを考える。

Laravelをインストール直後のUser.phpは以下のようになっていると思います。

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
      * The attributes that are mass assignable.
      *
      * @var  array
      */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
      * The attributes that should be hidden for arrays.
      *
      * @var  array
      */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
      * The attributes that should be cast to native types.
      *
      * @var  array
      */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
            

このUser.phpをバリデーションするUserRequest.phpを考えると、以下のようになると思います。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\User;

class UserRequest extends FormRequest
{
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return  bool
   */
  public function authorize()
  {
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return  array
   */
  public function rules()
  {
    $user = $this->user();

    return [
      'name' => [
        'required',
        'string',
        'max:64',
      ],
      'email' => [
        'required',
        'string',
        'max:64',
        'unique:App\User,email,'.optional($user)->id,
      ],
    ];
  }
}
            

そして、このUserRequest.phpをテストするクラスを考えます。
以下のTests\Requests\UserRequestTest.phpでは、setUp()でテスト用のデータベースを用意して、データを登録し、dataProvider()でテスト用のデータを作成し、testValidation()でテストを行い、tearDown()でもとに戻します。

<php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Artisan;
use App\Http\Requests\UserRequest;

class UserRequestTest extends TestCase
{
  use RefreshDatabase;
  use WithFaker;

  public function setUp()
  {
    parent::setUp();

    Artisan::call('migrate:fresh');

    $this->seed('UsersTableSeeder');
  }

  public function tearDown()
  {
    Artisan::call('migrate:reset');

    parent::tearDown();
  }

  /**
    * A basic feature test example.
    *
    * @return  void
    * @dataProvider    dataProvider()
    * @param  boolean
    * @param  arrays
    */
  public function testValidation($expect, $data)
  {
    $this->assertDatabaseHas(User::TABLE, [
      User::ID => '1',
    ]);

    $dataList  = $data();

    $request   = new UserRequest();

    $rules     = $request->rules();

    $validator = Validator::make($dataList, $rules);

    $result    = $validator->passes();

    $this->assertEquals($expect(), $result);
  }

  public function dataProvider()
  {
    $user = factory(\App\User::class)->make();

    $existing_user = User::findOrFail(1);

    return [
      'OK' => [
        'expect' => true,
        [
          'name'              => $user->name,
          'email'             => $user->email,
          'password'          => $user->password,
          'remember_token'    => $user->remember_token,
          'email_verified_at' => $user->email_verified_at,
        ],
      ],
      'NOK' => [
        'expect' => false,
        [
          'name'              => $existing_user->name,
          'email'             => $existing_user->email,
          'password'          => $existing_user->password,
          'remember_token'    => $existing_user->remember_token,
          'email_verified_at' => $existing_user->email_verified_at,
        ],
      ],
    ];
  }
}
            

dataProvider()で渡すデータの1つ目はファクトリで作成したダミーデータを渡して、テストしようとしています。
2つ目はデータベースのusersテーブルに登録済みのid=1のUserモデルを取得して、テストしようとしています。
一見、動きそうに見えますが、実はこのテストコードは動きません。
実際に動かして確認してみます。

vendor/bin/phpunit tests/Requests/UserRequestTest.php
PHPUnit 8.5.4 by Sebastian Bergmann and contributors.

W                                                                   1 / 1 (100%)

Time: 273 ms, Memory: 10.00 MB

There was 1 warning:

1) Warning
The data provider specified for Tests\Feature\UserRequestTest::testValidation is invalid.
InvalidArgumentException: Unable to locate factory for [App\User].
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:273
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:296
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:148
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:304
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:223
/Users/kondonator/Development/linq8.jp/tests/Requests/UserRequestTest.php:71

WARNINGS!
Tests: 1, Assertions: 0, Warnings: 1.
            

factory()を使うところでWarningが出てしまいました。
そこで、$user = factory(\App\User::class)->make();

'OK' => [
  'expect' => true,
  [
    'name'              => $user->name,
    'email'             => $user->email,
    'password'          => $user->password,
    'remember_token'    => $user->remember_token,
    'email_verified_at' => $user->email_verified_at,
  ],
],
            
をコメントにして、NOKの部分が実行されるようにして、再度、動かしてみましょう。

vendor/bin/phpunit tests/Requests/UserRequestTest.php
PHPUnit 8.5.4 by Sebastian Bergmann and contributors.

W                                                                   1 / 1 (100%)

Time: 72 ms, Memory: 10.00 MB

There was 1 warning:

1) Warning
The data provider specified for Tests\Feature\UserRequestTest::testValidation is invalid.
Error: Call to a member function connection() on null
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1342
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1308
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1114
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1031
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1067
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1020
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1728
/Users/kondonator/Development/linq8.jp/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1740
/Users/kondonator/Development/linq8.jp/tests/Requests/UserRequestTest.php:73

WARNINGS!
Tests: 1, Assertions: 0, Warnings: 1.
            

今度はnullのconnection()メソッドを呼んでいるというWarningが出てしまいました。

呼び出されるメソッドの順番を知る。

なぜこのようなことが起こるのでしょうか?
それは、phpunitがsetUp()メソッドよりも先にdataProvider()メソッドを評価するからです。
dataProvider()メソッドが評価される時点で、setUp()はまだ呼び出されていないため、データベースの準備ができていません。
この問題を解決するためのアプローチが遅延評価です。

App\Http\Requests\UserRequestTest.phpで遅延評価を使う。

それでは、遅延評価を行うように、クロージャを使って、UserRequestTest.phpを修正します。

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Artisan;
use App\Http\Requests\ValidationRule;
use App\Http\Requests\UserRequest;
use App\User;
use App\Role;

class UserRequestTest extends TestCase
{
  use RefreshDatabase;
  use WithFaker;

  public function setUp()
  {
    parent::setUp();

    Artisan::call('migrate:fresh');

    $this->seed('RolesTableSeeder');

    $this->seed('UsersTableSeeder');
  }

  public function tearDown()
  {
    Artisan::call('migrate:reset');

    parent::tearDown();
  }

  /**
    * A basic feature test example.
    *
    * @return  void
    * @dataProvider      dataProvider()
    * @param  callable
    * @param  callable
    */
  public function testValidation(callable $expect, callable $data)
  {
    $this->assertDatabaseHas(Role::TABLE, [
      Role::ID => '1',
    ]);

    $this->assertDatabaseHas(User::TABLE, [
      User::ID => '1',
    ]);

    $dataList  = $data();

    $request   = new UserRequest();

    $rules     = $request->rules();

    $validator = Validator::make($dataList, $rules);

    $result    = $validator->passes();

    $this->assertEquals($expect(), $result);
  }

  public function dataProvider()
  {
    return [
      'OK' => [
        'expect' => function () {
          return true;
        },
        function () {
          $user = factory(\App\User::class)->make();
        
          return [
            'name'              => $user->name,
            'email'             => $user->email,
            'password'          => $user->password,
            'remember_token'    => $user->remember_token,
            'email_verified_at' => $user->email_verified_at,
          ];
        },

      ],
      User::EMAIL.'_'.ValidationRule::unique(User::TABLE, User::EMAIL) => [

          'expect' => function () {
          return false;
        },
        function () {
          $existing_user = User::findOrFail(1);
        
          return [
            'name'              => $existing_user->name,
            'email'             => $existing_user->email,
            'password'          => $existing_user->password,
            'remember_token'    => $existing_user->remember_token,
            'email_verified_at' => $existing_user->email_verified_at,
          ];
        },

      ],
    ];
  }
}
            

ポイントは、クロージャの中にfactory()やデータベースへのアクセスを入れてしまうことです。
修正したら、実行してみます。

vendor/bin/phpunit tests/Requests/UserRequestTest.php
PHPUnit 8.5.4 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 5.96 seconds, Memory: 32.00 MB

OK (2 tests, 6 assertions)
            

テストがpassしました。
これで、UserRequestをテストする際に、factory()を読んだり、データベースに登録されているデータと照合することができるようになりました。
Laravelはphpunitを使いやすく作られているので、どんどんテストを作って、バージョンアップなどの際に自動テストで楽をできるようにしていきましょう!