勉強日記

チラ裏

LaravelのRDB絡みのテストを速くした話


結論

  • テスト始動に70秒かかっていたのが一瞬で始まるようになった
  • migrate:freshではなくmigrateを使う

背景

  • LaravelでRDB絡みのテストをするとき、いつもRefreshDatabaseトレイトを使っている
  • migrationの数が増えるにつれ、テストが耐え難い遅さになってきた

参考

動画

  • まさにこういう状態
    • 最初のRDB絡みのケースだけ遅い

原因

  • RefreshDatabaseトレイトのphp artisan migrate:fresh を実行している部分が遅い

コード

solution -- migrate:freshではなくmigrateを使用する

migrate:freshとmigrateの違い

  • migrate:freshでは、テーブルを削除して1からmigrateを行う
  • migrateでは、マイグレーション済みのテーブルについては処理をスキップする
  • テーブルを削除しなければならないケースは少ない
  • ふだんcode-test-commitをぐるぐる回すぶんにはmigrate:freshは不要で、migrateで十分

migrate使用による高速化

sample code -- RefreshDatabaseトレイトをoverrideする

  • 基本の考え方はDatabaseTransactionsトレイト + setUpでmigrate実行
  • が、いろいろ共通化したい
    • migrateの実行
    • 複数DBコネクション使用時、すべてをrollback対象にする
      • protected $connectionsToTransactメンバに設定
  • RefreshDatabaseトレイトのほうが完成像に近いので、こちらを手を加えて「オレオレRefreshDatabaseトレイト」を作る
<?php

declare(strict_types=1);

namespace Tests\Concerns;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
use App\Console\Kernel;

/**
 * migrate:freshの代わりにmigrateを使用する軽量版
 */
trait RefreshDatabaseLite
{
    use RefreshDatabase;

    /**
     * トランザクションロールバック対象のDBコネクション
     * @see RefreshDatabase@connectionsToTransact
     */
    protected $connectionsToTransact = [
        'mysql_foo',
        'mysql_bar',
        'mysql_secure',
    ];

    /**
     * Refresh a conventional test database.
     *
     * @return void
     */
    protected function refreshTestDatabase()
    {
        if (!RefreshDatabaseState::$migrated) {
            // ここを変えた
            $this->artisan('migrate');

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}
  • 個々のテストケース側で、Illuminate\Foundation\Testing\RefreshDatabase の代わりにTests\Concerns\RefreshDatabaseLiteをuseすればOK
  • 「トイレ行く前にテスト起動しとく」といった涙ぐましい時短はもはや必要ない
  • 世界は救われた