【Laravel】PHPUnitの実行でDBを汚染しないためにできる簡単なこと

# 環境

OS: Windows 10
PHP: 8.1.2
Laravel Framework: 8.81.0
docker-compose: 1.29.2
PHPUnit: 9.5.10
MySQL: 8.0

`Laravel Sail`を使用して、Laravelプロジェクトを立ち上げています。
プロジェクトを立ち上げる部分は、[こちらのドキュメント](https://readouble.com/laravel/8.x/ja/installation.html#your-first-laravel-project)でご確認ください。

# `RefreshDatabase` DBの状態に影響されずテストする

`use RefreshDatabase;`をしてあげると、
<font color="#f00">__テストを実行する際に前後のDBの状態を考える必要がなくなります__</font>。

各テスト毎にDBをリフレッシュするため、まっさらな状態にしてくれるので、
DBの状態を確認するアサーションである

- `$this->assertDatabaseCount(...);`
- `$this->assertDatabaseHas(...);`
- `$this->assertDatabaseMissing(...);`

あたりが扱いやすくなります。

そして、テストによって追加されてしまうデータも残らないので安心です。
では、実際に動作を見るためにテストを作成しましょう。

## テストの作成

まずは、テストの作成のために以下のコマンドを叩きます。

```bash:UserTestの追加
$ ./vendor/bin/sail artisan make:test Model/UserTest
```

そして、作成されたファイルを以下のように修正します。

```php:tests/Feature/Model/UserTest.php
<?php

namespace Tests\Feature\Model;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    const TABLE_NAME = 'users';

    protected function setUp(): void
    {
        parent::setUp();
    }

    /**
     * @return int
     * @throws \Exception
     */
    private function factory(): int
    {
        // 1～9のランダムな数値を取得
        $count = random_int(min: 1, max: 9);

        // $countの数だけ、新規にUserを作成する
        User::factory($count)->create();

        return $count;
    }

    /**
     * @test
     * @group Model
     * @group User
     *
     * @return void
     * @throws \Exception
     */
    public function 全件取得(): void
    {
        $count = $this->factory();

        // usersテーブルに$countの数だけデータが挿入されているか
        $this->assertDatabaseCount(table: self::TABLE_NAME, count: $count);

        $result = User::all();

        // 全件取得できているか
        $this->assertCount(expectedCount: $count, haystack: $result);
    }
}
```

さて、テストが作成できましたので、
これを動作させてみましょう。

## テストの実行

テストの実行のためには、以下のコマンドを叩きます。

```bash:テストの実行
$ ./vendor/bin/sail test --group=Model
```

今回のテストには`$this->assertDatabaseCount(...);`が含まれています。
もし、<font color="#f00">__DBの状態がリフレッシュされているならば、連続で実行しても問題ないはず__</font>です。

試しに3回ほど実行してみましょう！

```bash:実行結果
$ ./vendor/bin/sail test --group=Model

   PASS  Tests\Feature\Model\UserTest
  ✓ 全件取得

  Tests:  1 passed
  Time:   0.33s

$ ./vendor/bin/sail test --group=Model

   PASS  Tests\Feature\Model\UserTest
  ✓ 全件取得

  Tests:  1 passed
  Time:   0.30s

$ ./vendor/bin/sail test --group=Model

   PASS  Tests\Feature\Model\UserTest
  ✓ 全件取得

  Tests:  1 passed
  Time:   0.29s
```

問題なくPASSしていますね！

これで<font color="#f00">__DBの状態に影響されず__</font>テストができることが分かります。

## テスト後のDBの状態を確認

次にテストによって追加されてしまうデータが残っていないことを確認します。

確認方法は何でも良いのですが、今回はできる限り簡潔に画面に表示します。
以下の実装を行いましょう。

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    $users = \App\Models\User::all();

    dd($users);
});
```

あとは、[http://localhost/](http://localhost/)に接続すれば・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525820/75198788-3915-20c7-c31f-95cf55b5b56f.png)

出ました！！

`#items: []`ということは、何のデータも入っていないことなので、
これで<font color="#f00">__テストによって追加されてしまうデータが残っていない__</font>ことが分かりました。

## 本当に影響がないか確認

テストがデータに影響しないこともデータが残ってないことも分かりました。

では、<font color="#f00">__本当に何も影響がないのでしょうか？__</font>

それを確認するためにダミーデータを入れてからテストを実行してみましょう。

### ダミーデータの作成

まずは、ダミーデータを作成するためにシーダーを作成する必要がありますので、
シーダーを作成するために以下のコマンドを叩きます。

```bash:シーダーの作成
$ ./vendor/bin/sail artisan make:seeder UserSeeder
```

シーダーの作成ができたら、ダミーデータを作成するために実装しましょう。
以下の修正を加えればOKです。

```php:database/seeders/UserSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        User::factory(10)->create();
    }
}
```

```php:database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            UserSeeder::class,
        ]);
    }
}
```

これでダミーデータを作成する準備が整ったので、以下のコマンドを叩きましょう。

```bash:ダミーデータの作成
$ ./vendor/bin/sail artisan db:seed
```

作成しているかどうか、[http://localhost/](http://localhost/)に接続して確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525820/baa1e5f4-58d6-08fc-d3df-cc85477e40a9.png)

`#items: array:10 [▶]`となっているので作成できていますね。

### 結果は・・・

では、テストを実行してから確認してみましょう。

```bash:テストの実行
$ ./vendor/bin/sail test --group=Model
```

そして、[http://localhost/](http://localhost/)に接続・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525820/4946d203-ff19-bee9-1efb-f2692310d105.png)

き、消えてる・・・。

ということで、汚染しない代わりに<font color="#f00">__綺麗に全てを消してしまう__</font>のです。

## 解決方法

テスト実行の度にダミーデータを挿入するのも手間がかかりますし、
手動で追加したデータなどは消えてしまいます。
これでは、根本的な解決になりません。

根本的な解決方法を考えるなら
<font color="#f00">__テストのときは違うDB環境を使用__</font>すればよいのです。

### sqliteを使用する

設定は何もこだわらなければ、とても簡単です。

以下のように、コメントアウトを外すだけです。

```xml:phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>
```

これで設定完了です。

#### テスト実行（sqlite）

先程と同じようにダミーデータの作成し、テストを実行してみます。

```bash:ダミーデータの作成
$ ./vendor/bin/sail artisan db:seed
```

```bash:テストの実行
$ ./vendor/bin/sail test --group=Model
```

そして、[http://localhost/](http://localhost/)に接続・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525820/2e6baad2-31aa-5f8b-947a-db11275b6d4f.png)

やりました！
ダミーデータが残ったままです！

### テストで違うDB環境を使用する

簡単な設定でしたが、残念ながら
<font color="#f00">__外部キー制約などがあると、sqliteを使うことはできません・・・。__</font>

そんなときは、大人しく別のDBを用意する方法が良いでしょう。

#### `.env.testing`

まず、`.env.testing`を作成します。
一番楽なのは、`.env`を複製してリネームすることです。

重要なのは、
**`APP_KEY`にキーがあること**、**`DB_HOST`が`.env`と異なること**です。

今回は、`DB_HOST`を`mysql.test`とします。

```text:.env.testing
...

APP_KEY=（キーが入っている）

...

DB_CONNECTION=mysql
DB_HOST=mysql.test
DB_PORT=（.envと一緒）
DB_DATABASE=（.envと一緒）
DB_USERNAME=（.envと一緒）
DB_PASSWORD=（.envと一緒）

...
```

##### `APP_KEY`にキーが入っていない

その場合は、以下のコマンドで生成しましょう。

```bash:キーの生成
$ ./vendor/bin/sail artisan key:generate --env=testing
```

#### `docker-compose.yml`

先程、`.env.testing`で追加した`DB_HOST`を追加します。
今回は`mysql.test`ですね。

```yml:docker-compose.yml
    mysql.test:
        image: 'mysql/mysql-server:8.0'
        environment:
            MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ROOT_HOST: "%"
            MYSQL_DATABASE: '${DB_DATABASE}'
            MYSQL_USER: '${DB_USERNAME}'
            MYSQL_PASSWORD: '${DB_PASSWORD}'
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        networks:
            - sail
```

テスト用に使用するだけなので、
<font color="#f00">**ports、volumes、healthcheckなどは不要です。**</font>

追加した後は、`sail`を再度立ち上げましょう！

```bash:sailの立ち上げ
$ ./vendor/bin/sail up -d
```

#### テスト実行（別DB）

テストを実行してみます。

```bash:ダミーデータの作成
$ ./vendor/bin/sail artisan db:seed
```

```bash:テストの実行
$ ./vendor/bin/sail test --group=Model
```

そして、[http://localhost/](http://localhost/)に接続・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525820/2e6baad2-31aa-5f8b-947a-db11275b6d4f.png)

こちらでも問題なく残っていますね！

# まとめ

ということで、上記の手順によりDBの汚染を防ぐことができます。

テストを記述するのはとても大切だと思っているので、
備忘録ではありますが、この記事があなたの役に立てば嬉しいです。