【Laravel】Laravel 6.xがなかなか悩ましい(RefreshDatabase編)【メモ】

今参画しているプロジェクトで触っているプロダクトがLaravel6.xで動いていて、最近のバージョン(8.x以降)とはちょいちょい違う点があって悩ましいという話。






RefreshDatabase編

いろいろと「違うなあ」と思う部分はあって、例えばファクトリの書き方なんか8.xで大幅に変わって書きやすくなっているので、今さらそれ以前の書き方で書くの結構苦痛。しかも近い将来、新しいバージョン(出来れば10か11)に移行したいと思っている中で、旧資産を増やすのはどうなのよとも思うんですけど、でも今何もない状態だから仕方ない。満足にテストも書けないし。

で、そんな違いの中の1つにRefreshDatabaseの違いというのがあって、違いとしてはとても地味なんですけど、テストを書く上でどうしたもんじゃろなと思ったのでちょっとメモ書きがてら。



RefreshDatabaseとは

主にテストを実行するときにもので、これを指定しておくとデータベースを新規にセットアップしてくれるというトレイトです。設定によって多少変わりますが、6.xにおける基本的な動作はこんな感じになります。


  1. データーベースを全て削除
  2. マイグレーション実行
  3. トランザクション開始
  4. テスト実行
  5. ロールバック
  6. テスト実行
  7. ロールバック
  8. テスト実行
  9. ロールバック
  10. トランザクション終了


最初の2つ「データーベースを全て削除」「マイグレーション実行」がキモで、これを行うことによってテスト実行時のデータベース環境が常に同じ状態に保たれます。これにより「あるデータがあるときだけ動く、ないと動かない」といったデータベースに依存したテスト結果を排除しやすく出来ます。


問題点があるとするならば、マイグレーションを実行しただけではデータベースが空だということです。もちろんテストデータは各テストで随時作成していけばいいわけですけど、マスターデータ(カテゴリ一覧とか何かのタイプとか余り変更が生じない共通して使うデータ)までテストごとに入れるような処理をすると結構時間が掛かります。マスターデータではなくても、ほとんどのテストで使用する何十人か分のユーザーデータ(ダミーデータ)なんかも、毎回作ると時間が掛かります。テスト完了まで何十分とか普通になっちゃう。


この対策として2番目のマイグレーション実行時に同時にシーダーを動かして初期データをデータベースに入れてやるという解決案があります。8.x以降にはRefreshDatabaseトレイトにシーダーに関するオプションがあって、それを指定することで実現することが出来ます。


Laravel 9.x におけるRefreshDatabaseオプション

あるいは、RefreshDatabaseトレイトを使用する各テストの前に、自動的にデータベースをシードするようにLaravelに指示することもできます。これを実現するには、テストの基本クラスに$seedプロパティを定義します。

データベーステスト 9.x Laravel


「各テストの前に」という表現がわかりづらいですが、ソースコードを見てみるとに$seedプロパティを定義した場合に行われるシードは、マイグレーションと一緒に行われることになっています。


   protected function migrateFreshUsing()
   {
       $seeder = $this->seeder();


       return array_merge(
           [
               '--drop-views' => $this->shouldDropViews(),
               '--drop-types' => $this->shouldDropTypes(),
           ],
           $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()]
       );
   }
https://github.com/laravel/framework/blob/9.x/src/Illuminate/Foundation/Testing/Traits/CanConfigureMigrationCommands.php


つまり実行順はこうなります。


  1. データーベースを全て削除
  2. マイグレーションとシードを実行
  3. トランザクション開始
  4. テスト実行
  5. ロールバック
  6. (以下略)


Laravel 6.x におけるRefreshDatabaseオプション

一方で 6.x にはシードに関するオプションがありません。


機能テストでデータベースへ初期値を設定するために、データベースシーダを使いたい場合は、seedメソッドを使用してください。デフォルトでseedメソッドは、他のシーダを全部実行するDatabaseSeederを返します。もしくは、seedメソッドへ特定のシーダクラス名を渡してください。

データベースのテスト 6.x Laravel


テストの中で任意のシードクラスを実行することは出来ますが、それだと毎回シードをする必要があります。つまりこうなってしまいます。


  1. データーベースを全て削除
  2. マイグレーション実行
  3. トランザクション開始
  4. シード実行
  5. テスト実行
  6. ロールバック
  7. シード実行
  8. テスト実行
  9. ロールバック
  10. (以下略)


テストの数が少ないうちはこれでも良いんですけど、多くなってくるとさすがに大変。テストの内容に関連したデータはテストの中で作成するとしても、それ以外はあらかじめ登録しておいた初期データを使うようにしたいですよね。それがテストとしてベストプラクティスかどうかは自信がありませんけど、毎回全部作るのは実行速度的な意味で現実的ではないので……が、ない。どうする?



こんなコードを書いてみました

6.x を使いやすくすることに意味があるのか?という根源的な問題はありつつも、今テストが必要なので応急処理的にこんなのを追加。


   use RefreshDatabase;

   protected function setUp(): void
   {
       $do_seed = (!RefreshDatabaseState::$migrated);
       parent::setUp();
       // RefreshDatabse された直後だけシーダを実行する
       if ($do_seed && RefreshDatabaseState::$migrated) {
           $this->seed(TestDatabaseSeeder::class);
       }
   }


RefreshDatabaseState::$migrated は初期値が false でマイグレーションが実行されると true になります。これによって初回だけマイグレーションを実行することが担保されています。ということで Illuminate\Foundation\Testing\TestCase::setUp() でマイグレーションが実行された直後のタイミングを捕まえて、そこでテスト用のシードを実行しています。これなら 9.x とだいたい同じ処理が実現出来ます。


  1. データーベースを全て削除
  2. マイグレーション実行
  3. シード実行
  4. トランザクション開始
  5. テスト実行
  6. ロールバック
  7. (以下略)



根本的には:バージョンを上げましょう

というわけで 6.x でも効率的にシードが実行出来そうですが、しかし根本的にはなるべく早くバージョンを上げましょうねという話です。そもそも 6.x はもうセキュリティ修正もされていませんし、9.x でさえ 8/8 にバグフィックス対応が終わり、来年2月にはセキュリティ対応も終わるので。


……といっても 6.x からだと変更点が多くてそれだけで十分ツラいプロジェクトになりうるからなあ、、そもそもPHPのバージョンも変わるし。動かないこといろいろ出てきそう。いや確実に出てくる。


今動いているサービスじゃなければ、まずバージョンを上げてしまうことを考えても良いんでしょうけど、動いちゃってるしなあ、、悩ましいです。でもなるべく早く対応したいですね。年内にはなんとか10に移行したいなあ。