問題
LaravelでMySQLのDBテストを書いていたら、RefreshDatabaseでロールバックされないデータが発生した。
原因
テストで使っていたシーダー内でtruncate()を実行していたため。
解決方法
truncate()を使用しない。
状況の再現
たとえばhogeシーダーでは、truncate()で最初にテーブルデータの全件削除を行って、つぎにデータを投入している。
<?php
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class HogeSeeder extends Seeder
{
public function run()
{
DB::table('hoge')->truncate();
DB::table('hoge')->insert(['name'=>'事前投入ほげほげ']);
}
}
下記のようなDBテストで、RefreshSeederの実行&シーダーでのデータの流し込みをしているとする。
<?php
namespace Tests\Unit;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Hoge;
class HogeTest extends TestCase
{
use RefreshDatabase; // テストごとにデータをリフレッシュする
public function setUp(): void
{
parent::setUp();
$this->seed(); // テスト開始時にシーダを実行する
}
public function testHoge1()
{
// hogeテーブルにデータを追加する
$hoge = new Hoge();
$hoge->name = "テストほげほげ";
$hoge->save();
// このテスト内ではデータは存在するはず
$this->assertDatabaseHas('hoge', ['name' => "テストほげほげ"]);
}
public function testHoge2()
{
// 別のテストではtestHoge1()で追加したデータは存在しないことを期待している
$this->assertDatabaseMissing('hoge', ['name' => "テストほげほげ"]);
}
}
シーダーで流し込んだデータは全てのテストで共通して存在しており、各テストで追加したデータはそのテスト内のみで完結するように期待している。
しかしこのテストを実行すると、1つめのテストで追加したデータが残ったままで、2つめのテストは失敗する。
また、テスト終了後のテーブルの中身はシーディングデータ含めすべて残っている(ロールバックされていない)。
id | name |
---|---|
1 | 事前投入ほげほげ |
2 | テストほげほげ |
3 | 事前投入ほげほげ |
RefreshDatabaseとtruncate()を同時につかってはいけない
DB::table()->truncate()
はテーブルの全件削除のメソッドで、sqlのtruncateを実行する。
db:seed
時に前回のシーディングデータを削除したいがために使用されていたようだった。
一方、RefreshDatabaseはmirate:refresh
を実行し、トランザクションを貼って最後にロールバックするメソッドである。
これら2つが併用されるとどうなるかというと、下記の様になる。
--テスト開始--
-
migrate:refresh
実行 - トランザクッション開始
- シーダー実行
- truncateの実行(暗黙的なコミットがはしる)
- テストメソッドの処理
- tearDown()が呼ばれる
- ロールバックができない
--テスト終了--
sqlで書くとこんなかんじ。
BEGIN;
TRUNCATE `test`.`hoge`;
INSERT INTO `test`.`hoge` (`id`, `name`) VALUES ('1', '事前投入ほげほげ');
INSERT INTO `test`.`hoge` (`id`, `name`) VALUES ('2', 'テストほげほげ');
ROLLBACK;
実行するとわかるが、トランザクション内でtruncateを実行するとcommitが走るので、その後のinsertで追加したデータは残ったままである。
解決方法 truncateを使用しない
migrate:fresh --seed
すればいいだけなので、そもそもシーダーでtruncate()を呼ぶのはナンセンスである。
これだけのことを探すのにすごい時間がかかった。
参考:https://dev.mysql.com/doc/refman/8.0/ja/implicit-commit.html