【メモ】Laravel の RefreshDatabase が何をやっているのかまとめる【Laravel】

Laravelのテストで使用する「RefreshDatabase」トレイト。これを利用するとテスト実行時にデータベースをリフレッシュしてくれますが、実際の所何をしていて実行後にデータベースがどうなっているのかよくわからないのでソースコードから必要箇所を抜き出して調べてみます






トレイトの呼び出しは Illuminate\Foundation\Testing\TestCase にて

Illuminate\Foundation\Testing\TestCase L.124-128

$uses = array_flip(class_uses_recursive(static::class));

if (isset($uses[RefreshDatabase::class])) {
   $this->refreshDatabase();
}


トレイトが呼び出されていたらこれが実行されます。



処理本体は Illuminate\Foundation\Testing\RefreshDatabase

Illuminate\Foundation\Testing\RefreshDatabase L.17-26

public function refreshDatabase()
{
   $this->beforeRefreshingDatabase();

   $this->usingInMemoryDatabase()
                   ? $this->refreshInMemoryDatabase()
                   : $this->refreshTestDatabase();

   $this->afterRefreshingDatabase();
}


データベースとしてMySQLを利用している環境だとこんな感じ。


  1. beforeRefreshingDatabase()
  2. refreshTestDatabase()
  3. afterRefreshingDatabase()


このうち「beforeRefreshingDatabase()」「afterRefreshingDatabase()」に関してはここでは特に何も規定されておらず、必要に応じてテストもしくはテストの基底クラスで設定することになります(protected function)。よって実行されるのは実質的に「refreshTestDatabase()」のみです。



refreshTestDatabase() は何をしているか

Illuminate\Foundation\Testing\RefreshDatabase L.70-81

protected function refreshTestDatabase()
{
   if (! RefreshDatabaseState::$migrated) {
       $this->artisan('migrate:fresh', $this->migrateFreshUsing());

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

       RefreshDatabaseState::$migrated = true;
   }

   $this->beginDatabaseTransaction();
}


簡単に言えば次のコマンドを実行することと同義です。


php artisan migrate:fresh


このコマンドは以下の処理を行います。


  1. データベース上の全てのテーブルを削除
  2. マイグレーションを実行


つまりこの処理ではデータが空のテーブルが再生成されるということになります。すべてのテーブルを対象にtruncateが実行されるのと同義です。



Seederを起動させるためには

RefreshDatabase実行時にSeederを起動させることも可能です。引数の「$this->migrateFreshUsing()」を見ていきます。

Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands L.12-23

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

   return array_merge(
       [
           '--drop-views' => $this->shouldDropViews(),
           '--drop-types' => $this->shouldDropTypes(),
       ],
       $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()]
   );
}

Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands L.45-53

/**
* Determine if the seed task should be run when refreshing the database.
*
* @return bool
*/
protected function shouldSeed()
{
   return property_exists($this, 'seed') ? $this->seed : false;
}

Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands L.55-63

/**
* Determine the specific seeder class that should be used when refreshing the database.
*
* @return mixed
*/
protected function seeder()
{
   return property_exists($this, 'seeder') ? $this->seeder : false;
}


これらは以下の処理を行っていることがわかります。


  • クラスにプロパティ $seeder が存在していたら「–seeder=$seeder」オプションを付与して「artisan migrate:fresh」を実行する
  • クラスにプロパティ $seeder が存在せず、プロパティ $seed が存在していたら「–seed」オプションを付与して「artisan migrate:fresh」を実行する


1の場合はテーブルを空にしたあと特定の Seeder のみ実行します。


php artisan migratge:fresh –seeder=SampleSeeder


2の場合はテーブルを空にしたあと ​​DatabaseSeeder で規定された全ての Seeder を実行します。



テストクラスで指定出来ることまとめ

まとめます。


  • RefreshDatabase を実行するかどうか指定出来る
  • RefreshDatabase 実行前に何を行うか設定できる
  • RefreshDatabase 実行時に特定の Seeder を実行するように指定出来る
  • RefreshDatabase 実行時に規定された全ての Seeder を実行するように指定出来る
  • RefreshDatabase 実行後に何を行うか設定出来る



注意点:マイグレーションが実行されるのは1回だけ

この処理はテストクラスの setUp() 関数内で呼び出されるので、各テストメソッドの実行前に毎回呼び出されることになりますが、マイグレーションを実行したかどうかが RefreshDatabaseState::$migrated で管理されているため、マイグレーションが実行されるのはテスト全体を通して1回だけです。

例えばテストが3つ実行予定で、2つ目と3つ目のテストに RefreshDatabase が指定されているとすると、こういう順で処理されることになります。


  1. テスト1
  2. テスト2
  3. マイグレーション
  4. テスト3


テスト3で特定の Seeder クラスを呼び出すように設定していたとしても、テスト2で既にマイグレーションが実行されてしまっているため、テスト3のマイグレーションは実行されません。そういう意味で $seeder オプションは少々使いづらそうです。それよりもテストクラスの setUp() で親クラスの setUp() を呼び出したあとに db:seed を呼び出すように記述した方が実用的かも知れません。