0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FactoryBotで、特定の属性の値が異なる大量のテストデータを作りたい

Posted at

はじめに

RSpec などのテストで多数のレコードを作りたいとき、FactoryBot の create_list や build_list を使うとまとめて生成できて便利です。たとえば 100 件のレコードをまとめて作りたい場合は、次のように書きます。

create_list(:food, 100)

これで Food のレコードが 100 件作成・保存されます。ただし、もし 「レコードごとに別の値をセットしたい」 という場合はどうでしょうか?

たとえば、消費期限 (expiration_date) を 1 日ずつずらした 100 件のレコードを作りたい場合、次のように create_list にブロックを渡してみたくなるかもしれません。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

しかし、設定したはずのexpiration_dateを見てみると…

Food.pluck(:expiration_date)
# => [nil, nil, nil,,,,,]

せっかくブロックで expiration_date を設定したのにも関わらず、(初期値を設定していないこともあり)全てnilになってしまいます。この理由は create_list の挙動にあります。

create_list の挙動

create_list(:food, 100) の内部処理は、ざっくり以下のような流れです。

  1. food オブジェクトを FactoryBot で build(生成)
  2. food を save(DB 保存)
  3. ブロックが渡されていれば、その 作成済みオブジェクト をブロックに渡す
  4. ブロック内でオブジェクトを変更しても、追加の save は実行されない

つまり、「レコードが保存されてからブロックが呼ばれる」 ため、ブロック内で属性を設定しても、その変更はデータベースに反映されません。
これを回避するには、ブロックの中で明示的に save! する必要があります。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!  # ここで再度セーブしないとDBに反映されない
end

この方法であれば、それぞれ異なる expiration_date を持つ 100 件のレコードを作れます。ただし、1 つのオブジェクトに対して 2 回 save を行うことになり非効率です。

build_list + save! で作る場合

先述の問題を解決するには、以下のようにbulid_listを使うと良いです。

build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!
end

この書き方なら「生成 → 保存」の流れが素直になりますが、saveが100回走るため、SQL の発行回数も 100 回になります。テストデータがもっと膨大になると、パフォーマンスに悪影響を及ぼしかねません。

バルクインサート(insert_all)でまとめて保存する

大量データを扱う場合は、まとめて保存できる バルクインサート を利用する方法があります。
Rails 6 以降なら、insert_all を使ってまとめて挿入できます。

foods = build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

Food.insert_all(foods.map(&:attributes))

これなら、SQL の発行は 1 回 で済むため、大量のテストデータを高速に作成できます。
ただし、insert_all はレコードのコールバックやバリデーションは通らないという点に注意が必要です。厳密にコールバックやバリデーションを通す必要があるなら、この方法は使わずに従来の save をするなどの工夫が要ります。

まとめ

  • create_list にブロックを渡すと、オブジェクトは一度保存された後でブロックが呼ばれる
    → ブロック内で変更した属性がデータベースに反映されない
  • 強引にブロック内で再度 save! すれば反映されるが、1 オブジェクトにつき 2 回保存することになる
  • そもそも一括で属性を変えて保存したいだけなら build_list + save! の方が直感的
  • 大量データや性能重視の場合は build_list でオブジェクトを作ってから、insert_all で一度に保存すると高速

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?