Laravelのテストコード実行時間を18分から1分に短縮した話

Laravel

こんにちは!

つい先日、業務でデータベーステストのコードを書いていまして、実行してみると実行時間が18分もかかっていました。どうにかしてこれを短縮する方法はないかと色々と試行錯誤した結果、1分まで短縮することができました。

そこで、今回はLaravelのテストコード実行時間の短縮方法というテーマで書いていきます。

読者
読者

「Laravelのテストコードの実行時間が長すぎる…」

「Laravelのテストコード実行時間を短縮する方法が知りたい」

などお考えの方は参考になるかと思います。

それでは、見ていきましょう!

調査

短縮方法を説明する前に、現状を知るためにLaravelのテストコードの仕様を調べたので、こちらを見ていきます。

なお、前提として今回はデータベーステストに絞って話していきます。

Laravelで用意されているテストコード用のトレイトとして、RefreshDatabaseトレイト、DatabaseTransactionsトレイト、DatabaseMigrationsトレイトがあるかと思います。

これらは、ざっくりいうとテスト間で影響が出ないように各テストメソッド間でデータベースをリセットしてくれる機能です。

筆者の環境では、DatabaseMigrationsトレイトが使われており、これが3つの中で一番遅いようです。

それぞれどのような仕組みで動いているのかを軽く調べてみました。

すると、以下のような動作機構になっていることがわかりました。

  • RefreshDatabaseトレイト・・・各テストメソッド実行前にmigrate:freshを実行する。ただし、スキーマが最新であれば、データベースをマイグレートしない。
  • DatabaseTransactionsトレイト・・・テストを実行している間だけトランザクションを作り、テスト終了時にrollbackすることでテスト間で影響が出ないようにしている。データベースを完全にリセットしてくれる。
  • DatabaseMigrationsトレイト・・・各テストメソッド実行前にmigrate:freshを必ず実行してくれる。テスト間で影響が出ないようになっている。

この中で、DatabaseTransactionsトレイトとDatabaseMigrationsトレイトはどうやら古いようで、LaravelはRefreshDatabaseトレイトを使うことを推奨しているようです。

実行速度もRefreshDatabaseトレイト > DatabaseTransactionsトレイト、DatabaseMigrationsトレイトでした。

そこで、できればRefreshDatabaseトレイトを使っていきたいですが、いくつか問題点がありました。

  • 「スキーマが最新であれば、データベースをマイグレートしない」という仕様によって一部データが残ってしまっており、本来通るはずのテストが通らなかった
  • スキーマが最新でなければmigrate:freshを実行するが、そもそもこれも無駄がある。全てのテーブルを対象にする必要はない

ということで、最速で実行するために手動でデータベースリセットを行うという方針で実装を進めることにしました。

テストコード実行時間を短縮する方法

ユニットテスト内でsetUp()メソッドをオーバーライドして、migrateと必要なテーブルのみtruncateを行なっていきます。

public function setUp(): void
{
    parent::setUp();
    $this->artisan('migrate');
    $this->truncateDatabase(['teams', 'users', 'mails']);
    ...
}

次に、親クラスであるTestCaseクラスにtruncateするメソッドを書いておきます。

protected function truncateDatabase($tableNames): void
{
    // 外部キー制約を無効化
    Schema::disableForeignKeyConstraints();
    foreach ($tableNames as $name) {
        // マイグレーションテーブルのみ削除しない
        if ($name === 'migrations') {
            continue;
        }
        // 各テーブル毎にtruncateでデータ削除
        DB::table($name)->truncate();
    }
    // 外部キー制約を有効化
    Schema::enableForeignKeyConstraints();
}

ここで、テーブルによっては外部キーを一時的に無効化しておかないとtruncateできないので注意してください。

こちらで実行をかけると、必要な分だけmigrateとtruncateを行い、リセットしてくれるので無駄がないです。

これで、実行時間1分を実現できました。

最後に、筆者が試した際の実験結果を載せておきます。

まとめ

まとめると、以下のようになると思います。

  • Laravelのデータベースリセット用のトレイトには、RefreshDatabaseトレイト、DatabaseTransactionsトレイト、DatabaseMigrationsトレイトがある
  • 実行速度はRefreshDatabaseトレイト > DatabaseTransactionsトレイト、DatabaseMigrationsトレイト
  • 手動でリセットをかけるのが最速

テストコードに限らずですが、どこがボトルネックになっているのかを特定して改善するというのはプログラマーに必要な能力です。

既存コードに疑いの目を持って、もっと良くできないか?を常に問い続けていきましょう。

それでは、今回はここまでです。ご清聴ありがとうございました!

参考

PHPUnitテストを高速化した話 | R Tech Blog
Laravelを使ったアプリケーションでテストコード(PHPUnit)を書くことはよくあることだと思います。 その中でもデータベースを利用したテストを実行したいことも多いはずです。 またCircleCIなどのCI/CDツールを利用してテスト
Laravel(PHP8) RefreshDatabaseが使えない!? - Qiita
概要RefreshDatabaseをPHP8系で利用するとエラーになり、ユニットテストができないようです。対処方法はいくつか考えられますが、おすすめの2パターンを紹介します。プロジェクトやテス…

コメント

タイトルとURLをコピーしました