Laravel PHPUnitのdataProviderでFacadeやfactory等のヘルパ関数を使う方法

追記: 2020/08/12 Laravel6.0以降はこの方法だとエラーになるため、遅延評価をお試しください。
参考になる記事をご紹介します。

- [【Laravel5.8】dataProviderでFacade等を使う/遅延評価でやってみる](https://wand-ta.hatenablog.com/entry/2019/09/04/204622)

## 問題のテストコード

```php
<?php

namespace Tests\Unit;

use App\User;
use Hash;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * @param string $name
     * @param array $data
     * @param string $dataName
     */
    public function __construct($name = null, array $data = [], $dataName = '')
    {
        parent::__construct($name, $data, $dataName);
        $this->createApplication();
    }

    /**
     * @param User $user
     * @param string $hash
     * @return void
     * @dataProvider dataProvider
     */
    public function testBasicTest(User $user, string $hash): void
    {
        $this->assertNotNull($user);
        $this->assertNotNull($hash);
    }

    /**
     * @return array
     */
    public function dataProvider(): array
    {
        $data = [];
        foreach (range(1, 100) as $i) {
            $data["TestCase$i"] = [
                'user' => factory(User::class)->create(),
                'hash' => Hash::make('secret'),
            ];
        }

        return $data;
    }
}
```

```shell-session
$ ./vendor/bin/phpunit
PHPUnit 7.5.15 by Sebastian Bergmann and contributors.

W                                                                   1 / 1 (100%)

Time: 323 ms, Memory: 6.00 MB

There was 1 warning:

1) Warning
The data provider specified for Tests\Unit\ExampleTest::testBasicTest is invalid.
Unable to locate factory with name [default] [App\User].

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

Warningが表示されます。

## 原因

- [php - Laravel framework classes not available in PHPUnit data provider - Stack Overflow](https://stackoverflow.com/questions/26710631/laravel-framework-classes-not-available-in-phpunit-data-provider)
- [PHPUnit, Using class consts to change State](https://stackoverflow.com/questions/5561844/phpunit-using-class-consts-to-change-state/5571631#5571631)

PHPUnitが任意のテストケースでデータプロバイダーとメソッドの呼び出し順序に起因する問題のようです。

## 解決方法1: __constructにcreateApplicationを追加する

```php
    /**
     * @param string $name
     * @param array $data
     * @param string $dataName
     */
    public function __construct($name = null, array $data = [], $dataName = '')
    {
        parent::__construct($name, $data, $dataName);
        $this->createApplication();
    }
```

コンストラクタをオーバーライドして、 `$this->createApplication();` を追記してあげればokです。
これで `dataProvider` 内でファサードやヘルパ関数を呼び出せます。

## [推奨] 解決方法2: dataProviderにcreateApplicationを追加する

```
    /**
     * @return array
     */
    public function dataProvider(): array
    {
        $this->createApplication();

        $data = [];
        foreach (range(1, 100) as $i) {
            $data["TestCase$i"] = [
                'user' => factory(User::class)->create(),
                'hash' => Hash::make('secret'),
            ];
        }

        return $data;
    }
```

## 解決方法1 と 解決方法2 でテスト実行

### 解決方法1でテスト実行 

```shell-session
$ ./vendor/bin/phpunit
PHPUnit 7.5.15 by Sebastian Bergmann and contributors.

...............................................................  63 / 100 ( 63%)
.....................................                           100 / 100 (100%)

Time: 18.16 seconds, Memory: 46.00 MB

OK (100 tests, 200 assertions)
```

### 解決方法2でテスト実行 

```shell-session
$ ./vendor/bin/phpunit
PHPUnit 7.5.15 by Sebastian Bergmann and contributors.

...............................................................  63 / 100 ( 63%)
.....................................                           100 / 100 (100%)

Time: 8.67 seconds, Memory: 22.00 MB

OK (100 tests, 200 assertions)
```

## 解決方法1 と 解決方法2 でテスト結果

```
解決方法1: Time: 18.16 seconds, Memory: 46.00 MB
解決方法2: Time: 8.67 seconds, Memory: 22.00 MB
```

100件のテストデータで10秒、メモリ量は2倍の差が出ました。
テストケースが多い場合はテストの初期化でテストの実行速度に明らかな差が出るので解決方法2を推奨します。

- [Tidying Up Your PHPUnit Tests with Data Providers](https://tighten.co/blog/tidying-up-your-phpunit-tests-with-data-providers)

### 補足: createApplication

`tests/CreatesApplication.php` Laravelが元々用意しているトレイトの関数を呼び出してます。
Laravelの初期化処理を行われるので、Facadeが読み込めるようになります。

```php
<?php

namespace Tests;

use Illuminate\Contracts\Console\Kernel;

trait CreatesApplication
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(Kernel::class)->bootstrap();

        return $app;
    }
}
```