chocolate Tech room

チョコを食べながら技術のことを考える

【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 を使うのが手軽

シナリオによっては関連データもそれぞれ細かに定義しなきゃいけない場合がありますが、そうなった場合のベストプラクティスはまだ見つかっていません。