【Laravel】Factory定義の中で Factoryのcreateはやめよう
Factoryの挙動を勘違いしていたせいで、無駄なデータを大量生成してしまったお話です。
例
こんなテーブル構成とします。
users - id - name ... shops - id - name ... orders - id - user_id - shop_id - amount ...
OrderFactory
をこんな書き方にしている方は、今すぐやめましょう。
<?php // OrderFactory.php $this->define(Order::class, function (Faker $faker) { return [ 'user_id' => factory(User::class)->create()->id, 'shop_id' => factory(Shop::class)->create()->id, 'amount' => ..., ]; });
Feature テストや Seeder で使ったとき、あなたが意図しないデータが生成されてしまいます。
どういうことなのか?
実際に Feature テストを書くときは、Faker の値をそのまま利用せず、いくつかのプロパティはなんらかの値に固定するかと思います。
今回は User
を生成する際に name
を固定値にしています。
<?php $user = factory(User::class)->create(['name' => 'test']); factory(Order::class)->create([ 'user_id' => $user->id, ]);
この直後に User::all()
すると、2つ の User
が取得できてしまいます!
つまり上のコードは、
<?php $user = factory(User::class)->create(); $order = factory(Order::class)->create(); $order->fill(['user_id' => $user->id])->save();
こう書いているのと変わらない、ということですね。
単なるテストならこれでもまあ問題にはなりにくいですが、API 連携テスト環境の Seeder で呼ばれる、全ての Factory がこんな書き方をしていたら……
あら不思議、想定の 30 倍近いデータが出来上がります!!!(経験談)
いちいち関連データのcreate書くのが面倒
とはいえ、Order
に関する全てのテストで User
の値を固定したい」なんてことはないでしょう。
何でもいいから User
と紐付けたい、ということのほうが多いかもしれません。
そういうときは、別名をつけるか、 state
を使っています。
別名をつける場合
<?php // OrderFactory.php $this->define(Order::class, function (Faker $faker) { return [ 'user_id' => factory(User::class)->create()->id, 'amount' => ... ]; }, 'with_user_id'); // test, seeder factory(Order::class, 'with_user_id')->create();
state を使う場合
https://laravel.com/docs/7.x/database-testing#factory-states
<?php // OrderFactory.php $this->define(Order::class, function (Faker $faker) { return [ ..., ]; }); $this->state(Order::class, 'with_user_id', [ 'user_id' => factory(User::class)->create()->id, ]; }); // test, seeder factory(Order::class)->state('with_user_id')->create();
まとめ
factory(Eloquent::class)->create()
にはプロパティを渡せるが、元のFactoryで定義した値を無視して生成してくれるわけではない- 関連データの生成を書くのが面倒なら、別名をつけるか、
state
を使うのが手軽
シナリオによっては関連データもそれぞれ細かに定義しなきゃいけない場合がありますが、そうなった場合のベストプラクティスはまだ見つかっていません。