🧹

Laravelでテスト毎にテーブルを初期化する

2023/05/24に公開

TANOMUではLaravelのテストで実際にDBに書き込む処理もたくさんあります。

実際にレコードを書き込むので、書き込んだままにしておくと別のテストケースに対して影響を与えてしまいます。

そこで当初は牧歌的に書き込んだテーブルの初期化処理を以下のように逐一書いていました。
実際はもう少し共通化してましたが、ざっくりこんなイメージです。

protected function tearDown(): void {
    DB::table('foos')->truncate();
    DB::table('bars')->truncate();

    parent::tearDown();
}

ただ、一定量超えてきたあたりで

  • そもそも宣言するのが面倒くさい
  • truncate漏れがあったが別のテストケース追加時にそれが発覚する
  • 全部のテストケースで全部をクリアする必要がない(過剰なtruncate実行)

といった問題が発生し、難しみを感じていたので、現在は以下のようにメタい感じで処理するようにしています。

<?php
namespace Tests;

use DB;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected array $usedTableNames = [];

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

        $this->initializeQueryListener();
    }

    protected function tearDown(): void
    {
        $this->clearUsedTables();

        parent::tearDown();
    }

    protected function initializeQueryListener(): void
    {
        // INSERT/UPDATE/DELETEクエリを元に更新があったテーブル名をusedTableNamesに保持していく
        $this->usedTableNames = [];
        DB::listen(function (QueryExecuted $sql) {
            preg_match(
                "/^(?:insert into|update|delete from) `?([^`]+)`? /i",
                $sql->sql,
                $matches
            );
            // SQLからテーブル名を取得できたら格納していく
            if ($matches[1] ?? null) {
                $this->usedTableNames[] = $matches[1];
            }
        });
    }

    protected function clearUsedTables(): void
    {
        // insert/update/deleteが走ったテーブルの中身を消していく
        if (count($this->usedTableNames) > 0) {
            $tableNames = array_unique($this->usedTableNames);
            DB::statement("SET FOREIGN_KEY_CHECKS = 0;");
            foreach ($tableNames as $tableName) {
                DB::table($tableName)->truncate();
            }
            DB::statement("SET FOREIGN_KEY_CHECKS = 1;");
        }
    }
}

上記はMySQLでの例になります。
以下のような処理を行っています。

  • setUp()DB::listen() を用いてクエリを監視開始する
  • SQLをパースして更新系のクエリがあった場合はテーブル名をプロパティに格納する
  • 同様に tearDown() で更新系のクエリがあったテーブルをtruncateしていく

※SQLのパース部分はプロジェクトによって多少変える必要はあるかもしれません。

もっと良いソリューションがある気はしますが、いまのところこの処理でそこまで不満なくテストケースを作れています。

TANOMU

Discussion