MySQL と PostgreSQL など、Laravel で複数のコネクションを使ってるときに RefreshDatabase
を使ったテストを書くと、二度目以降の実行でエラーになります。
調べてみると、その原因は RefreshDatabase
で使われている migrate:fresh
にあることが分かりました。1
migrate:fresh
では db:wipe
と migrate
の 2 つのコマンドが呼ばれているのですが、db:wipe
がデフォルトのコネクションに対してしか実行されないのです。2
RefreshDatabase
のコードを見てみると、以下のような使用するコネクションを指定するための仕組みが用意されており、これを使えばうまくいくような気がしたんですけど、ここで指定したコネクションは、トランザクションでのみ使われるようでした。3
DatabaseTransactions
にもこれと同じコードがあるので、そちらから派生させたときの名残なんでしょうね。4
/**
* The database connections that should have transactions.
*
* @return array
*/
protected function connectionsToTransact()
{
return property_exists($this, 'connectionsToTransact')
? $this->connectionsToTransact : [null];
}
じゃあ migrate:fresh
の方でもこの値を使うようにしたいと思ったのですが、migrate:fresh
では db:wipe
と migrate
がセットになってしまっているため、db:wipe
を 2 回実行するようにすると、migrate
まで 2 回実行されてしまいます。おのれ密結合。
しかも、migarate
の --database
オプションは migrations
テーブルを作るデータベースを指定するためのものなので、たとえ複数のコネクションを使っていたとしても、その中からひとつだけ指定すればよいものです。
一方で、db:wipe
の --database
オプションはテーブルを削除するデータベースを指定するためのものなので、複数コネクションを使っている場合は、その全てを指定する必要があります。
というように、それぞれ意味が違うのですが、migrate:fresh
では --database
オプションで受け取った値を、その両方に渡してしまっています。2
そのため対策としては、migrate:fresh
を使うのをやめて、代わりに migrate:refresh
を使うようにするか、db:wipe
と migrate
を自分で呼ぶようにするしかなさそうです。
以下に実装例を挙げておきます。
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase as BaseTrait;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
trait RefreshDatabase
{
use BaseTrait;
/**
* Refresh a conventional test database.
*
* @return void
*/
protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
$this->artisan('migrate:refresh');
$this->app[Kernel::class]->setArtisan(null);
RefreshDatabaseState::$migrated = true;
}
$this->beginDatabaseTransaction();
}
}
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase as BaseTrait;
use Illuminate\Foundation\Testing\RefreshDatabaseState;
trait RefreshDatabase
{
use BaseTrait;
/**
* Refresh a conventional test database.
*
* @return void
*/
protected function refreshTestDatabase()
{
if (! RefreshDatabaseState::$migrated) {
foreach ($this->connectionsToTransact() as $database) {
$this->artisan('db:wipe', array_filter([
'--database' => $database,
'--drop-views' => $this->shouldDropViews(),
'--drop-types' => $this->shouldDropTypes(),
'--force' => true,
]));
}
$this->artisan('migrate', [
'--force' => true,
]);
$this->app[Kernel::class]->setArtisan(null);
RefreshDatabaseState::$migrated = true;
}
$this->beginDatabaseTransaction();
}
}
-
https://github.com/laravel/framework/blob/6.x/src/Illuminate/Foundation/Testing/RefreshDatabase.php#L45-L64 ↩
-
https://github.com/laravel/framework/blob/6.x/src/Illuminate/Database/Console/Migrations/FreshCommand.php#L40-L53 ↩ ↩2
-
https://github.com/laravel/framework/blob/6.x/src/Illuminate/Foundation/Testing/RefreshDatabase.php#L75-L94 ↩
-
https://github.com/laravel/framework/blob/6.x/src/Illuminate/Foundation/Testing/DatabaseTransactions.php#L30-L39 ↩