FactoryBotの定義チートシート
factory_bot 6.5.0、factory_bot_rails 6.4.4で確認。
利用方法はFactoryBotの利用方法チートシートを参照。
定義ファイル
RSpecを使っている場合、デフォルトでは spec/factories/[modelファイル名の複数形].rb
になる。例えば spec/factories/users.rb
など。
しかし、ファイルが別れていたとしても名前空間が別れたりするわけでもなく、factoryの定義はグローバルに定義される。例えば、以下のような設定をした場合は FactoryBot::DuplicateDefinitionError
でエラーになる。
# spec/factories/users.rb
factory :admin, class: 'User' do
account_name { 'admin_account' }
end
# spec/factories/admins.rb
factory :admin do
permission_type { :full }
end
そのため、ファイル名に何らかの意味があるかといえば、挙動の面において何の影響力もなく任意のファイル名にすることができる。さらにspec/factories/foo/bar.rb
のようにapp/models/
配下のディレクトリ構成とは別の構成でディレクトリを作ることもできる。
ただ、可能であるという話であり、わざわざ標準から外れる選択肢をとるかは別問題である。
基本定義
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { 'suzuki' }
end
end
FactoryBot.define
のブロックは定型文。factory
メソッドの引数のシンボル(以下、Factory名とする)は、build
メソッドなどに渡す第一引数に対応する。Factory名がModel名のスネークケースに一致する場合は、そのModelのインスタンスが作られる。
上記例のname
はusersテーブルのカラム名で、ブロックには初期値を指定する。つまり、上記定義がある場合はFactoryBotを使う場合と直接Modelをインスタンス化する場合は以下のように対応する。
build(:user)
User.new(name: 'suzuki')
create(:user)
User.create(name: 'suzuki')
古い文献では name { 'suzuki' }
の代わりに name 'suzuki'
とブロックなし定義がされていることがあるが、これはfactory_bot v5.0.0で削除された記法であり、現代ではブロックを指定する記法しかサポートされていない。rubocop-factory_bot
を入れているのであれば、FactoryBot/AttributeDefinedStaticallyによって古い記法での記述は警告される。
インスタンスを作るクラスの指定
前述の通り、Factory名がModel名のスネークケースならインスタンス化されるのはそのModelになる。Factory名がModel名に対応しない場合はクラス名を明示的に指定する必要がある。
# spec/factories/users.rb
FactoryBot.define do
factory :admin, class: 'User' do
name { 'suzuki' }
end
end
class
キーワード引数に指定する値は、文字列でなくとも class: User
のようにクラスの定数を指定することも可能ではある。しかしrubocop-factory_bot
を入れているのであれば、FactoryBot/FactoryClassNameで警告されるため文字列を指定しておくべきである。
blongs_toアソシエーション、has_oneアソシエーション
相互に接続したアソシエーションの問題は別記。
FactoryBotの定義においては、blongs_toもhas_oneも区別がない。
例えばBookモデルが以下のようになっていたとする。
class Book < ApplicationRecord
belongs_to :author
end
この場合、spec/factories/books.rb
は以下のようにすることで、author
アソシエーションを定義できる。
FactoryBot.define do
factory :book do
# spec/factories/authors.rb に `factory :author`の定義がある前提
author
end
end
なお、association :author
のように書くことも可能だが、調べた限りでは違いはない。また、rubocop-factory_bot
を入れているのであれば、FactoryBot/AssociationStyleで警告されるので、factoryメソッドブロックの直下ではassociationは使わない方が良いだろう。オプションで変えられるので好み次第ではある。
has_manyアソシエーション
相互に接続したアソシエーションの問題は別記。
例えばAuthorモデルが以下のようになっていたとする。
class Author < ApplicationRecord
has_many :books
end
この場合、spec/factories/authors.rb
は以下のようにすることで、books
アソシエーションを定義できる。
FactoryBot.define do
factory :author do
# spec/factories/books.rb に `factory :book`の定義がある前提
books { Array.new(5) { association(:book) }.compact }
end
end
一見するとbooks { build_list(:book, 5) }
でも良さそうに思えるが、build_stubbed
、attributes_for
メソッドで思った挙動にならないためやめた方が良い。build_stubbed
メソッドではidなどに値が入らないし、attributes_for
メソッドではbooks
キーにBookモデルのインスタンスの配列が入ってしまう。
compact
を付けている理由は、attributes_for
メソッドを呼んだ時に { books: [nil, nil, nil, nil, nil] }
のようになってしまうため。compact
なしだとAuthor.new(attributes_for(:author))
はActiveRecord::AssociationTypeMismatch
でエラーになってしまうが、compact
をつけることでエラーを回避できる。
作成するbooksアソシエーションの数を制御したい場合は、transient
を用いる。
FactoryBot.define do
factory :author do
transient do
books_count { 5 }
end
# spec/factories/books.rb に `factory :book`の定義がある前提
books { Array.new(books_count) { association(:book) }.compact }
end
end
このような定義をすることでbuild(:author, books_count: 10)
のようにインスタンス生成時に生成個数を制御できるようになる。
after(:create)
などのコールバックを使って定義する方法もあるようだが、おそらくは上記が正攻法である。少なくとも、build
、create
、build_stubbed
、attributes_for
のいずれもうまくいくシンプルな記述方法になる。
after(:create)
などを使った方法については公式のhas_many associationsのセクションを確認してほしい。
相互に接続したアソシエーション
以下のような相互に接続したアソシエーションがある場合、そのままFactoryBotの定義に記述してしまうと無限ループに陥ってしまう。
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
FactoryBot.define do
factory :author do
books { Array.new(books_count) { association(:book) }.compact }
end
end
FactoryBot.define do
factory :book do
author
end
end
build(:author)
を呼んだ場合、factory :author
内のbooksアソシエーションからbuild(:book)
が呼ばれ、factory :book
の定義が読まれauthorアソシエーションからbuild(:author)
が呼ばれ…という形になり無限ループとなる。
この無限ループを断ち切る方法は、association
メソッドの引数でアソシエーションのオーバーライドをしてしまえば良い。
FactoryBot.define do
factory :author do
books { Array.new(books_count) { association(:book, author: instance) }.compact }
end
end
FactoryBot.define do
factory :book do
author { association(:author, books: [instance]) }
end
end
instance
は各々のアソシエーションを持つインスタンスになる。そのため、以下の通り整合性の取れたデータとなる。
author = create(:author)
author.books.first.author == author # => true
アソシエーション名と異なるFactory名を指定する
何らかの理由でアソシエーション名とFactory名が異なる場合、factory
キーワード引数を指定することで目的を達成できる。
FactoryBot.define do
factory :book do
author factory: :user
end
end
Factory名のエイリアス
機能として存在するので記載するが、「アソシエーション名と異なるFactory名を指定する」方法をとった方がわかりやすいように思う。
factory
メソッドの引数にaliases: [...]
を指定することでFactory名にエイリアスをつけることができる。
# spec/factories/users.rb
FactoryBot.define do
factory :user, aliases: [:author] do
name { 'suzuki' }
end
end
これは、Model名とアソシエーション名が異なる場合に有用になる。例えば、Bookモデルのアソシエーション定義が以下のようになっていたとする。
class Book < ApplicationRecord
belongs_to :author, class_name: 'User'
end
このBookモデルに対して以下のようにuser
を用いてアソシエーションを定義することはできない。
# spec/factories/books.rb
# これはうまくいかない
FactoryBot.define do
factory :book do
user
end
end
# こっちはうまくいく
FactoryBot.define do
factory :book do
author
end
end
他属性の参照
他の属性の値を参照することもできる。
FactoryBot.define do
factory :user do
name { 'suzuki' }
email { "#{name}@example.com" }
end
end
このようにするとbuild(:user)
とした時のemailの値はsuzuki@example.com
となる。また、build(:user, name: 'saito')
とした時はemailの値はsaito@example.com
となる。
属性だけでなくアソシエーションも参照することができる。例えば、ブログホスティングサービスで、以下のようなモデルが定義されているとする。
class User < ApplicationRecord
has_many :blogs
has_many :entries
end
class Blog < ApplicationRecord
belongs_to :user
has_many :entries
end
class Entry < ApplicationRecord
belongs_to :blog
# blog経由で到達できるがショートカットで外部キーがあるとする
belongs_to :user
end
このような場合、BlogのFactoryではblog.user
とblog.entries.first.user
が一致するようにしなければならないが、アソシエーションも参照可能なので以下のように対処ができる。
FactoryBot.define do
factory :blog do
user
entries { Array.new(5) { association(:entry, blog: instance, user: user) }.compact }
end
end
FactoryBot.define do
factory :entry do
blog { association(:blog, entries: [instance], user: user) }
user
end
end
ポイントは、association
メソッドの引数user: user
である。名前が同じだからわかりづらいが、association(:blog, entries: [instance], user: user)
の場合、Blogから参照するUserのインスタンスにEntryですでにインスタンス化しているUserのインスタンスを指定している。
シーケンス
自動連番を生成する仕組みもある。
FactoryBot.define do
# factoryメソッドのブロック外である点に注意
sequence :account_name do |n|
"account#{n}"
end
end
これを利用するには、generate
メソッドを利用する。
FactoryBot.define do
factory :user do
account_name { generate(:account_name) }
end
end
上記の例のようにSequence名と属性名が一致している場合は単に以下のようにできる。
FactoryBot.define do
factory :user do
account_name # シーケンスで生成された値になる
end
end
ここまでの例はグローバルなシーケンスの例だが、属性単位でシーケンスを定義することもできる。
FactoryBot.define do
factory :user do
sequence(:account_name) { |n| "account#{n}" }
end
end
sequence
メソッドの第二引数にシーケンスの初期値を指定することもできる。
FactoryBot.define do
factory :user do
sequence(:account_name, 50) { |n| "account#{n}" }
end
end
この初期値は、next
メソッドに応答するイテレータであれば何でも良い。
FactoryBot.define do
factory :user do
# :a, :b ... :z, :aa, :ab ...
sequence(:account_name, :a) { |n| "account#{n}" }
# 'a', 'b' ... 'z', 'aa', 'ab' ...
sequence(:account_name, 'a') { |n| "account#{n}" }
# 同じ値が生成され得るがこういうことも可能
sequence(:account_name, ['suzuki', 'tanaka', 'saito'].cycle) { |n| "account#{n}" }
end
end
また、ブロックを省略した場合はブロック引数の値がそのまま設定される。
sequence
メソッドにaliases
キーワード引数を指定することでSequence名のエイリアスを作れるようであるが、試した限りではおそらくグローバルなシーケンスでしか利用できない。
ファクトリのネストと継承
factory
メソッドはネストすることができる。
FactoryBot.define do
factory :user do
name { 'suzuki' }
email { "#{name}@example.com" }
factory :admin do
name { 'admin' }
end
end
end
build(:admin)
を実行した際はUser.new(name: 'admin', email: 'admin@example.com')
が実行されたのと同じ結果となる。
つまり、親の属性などは引き継ぐし、子で定義した属性があればそちらが優先される。
また、ネストせずに特定のファクトリから引き継ぐ記法もある。
FactoryBot.define do
factory :user do
name { 'suzuki' }
email { "#{name}@example.com" }
end
# ネストしていないが、parentを指定していることでuserファクトリから属性を引き継いでいる
factory :admin, parent: :user do
name { 'admin' }
end
end
トレイト
factory
メソッドをネストすれば属性の継承ができるため、共通部分を親で定義して子でバリエーションを持たせるということはできる。
しかし、例えばProductモデルのファクトリを定義する際に、セール品、定価品と物理商品と電子商品という切り口でファクトリを用意するとなると面倒なことになる。少なくとも片方の切り口はダブらせて書かざるをえない。
FactoryBot.define do
factory :product do
factory :sale_product do
# セール品の属性が並ぶ
factory :sale_physical_product do
# 物理商品の属性が並ぶ
end
factory :sale_electronic_product do
# ...
end
end
factory :regular_price_product do
# 定価品の属性が並ぶ
factory :regular_price_physical_product do
# sale_physical_productのところと全く一緒のものが並ぶ
end
factory :regular_price_electronic_product do
# sale_electronic_productのところと全く一緒のものが並ぶ
end
end
end
end
こういう場合はtrait
メソッドを利用すると便利である。
FactoryBot.define do
factory :product do
trait :sale_product do
# セール品の属性
end
trait :regular_price_product do
# 定価品の属性
end
trait :physical_product do
# 物理商品の属性
end
trait :electronic_product do
# 電子商品の属性
end
factory :sale_physical_product, traits: [:sale_product, :physical_product]
factory :sale_electronic_product, traits: [:sale_product, :electronic_product]
factory :regular_price_physical_product, traits: [:regular_price_product, :physical_product]
# 以下のようにトレイトを属性のように書くことも可能
factory :regular_price_electronic_product do
regular_price_product
electronic_product
end
end
end
複数指定しているトレイトで同じ属性の定義をしている場合は、配列の最後側のトレイトの定義が優先される。
以下のように、ファクトリ定義とトレイトの属性が衝突した場合は常にファクトリ定義の物が優先される。
trait :admin_name do
name { '管理者' }
end
# build(:admin).name # => 'admin'
factory :admin do
# トレイトと属性の順序に関係なく属性定義の内容が優先される
name { 'admin' }
admin_name
end
トレイトはfactory
だけではなく、association
メソッドでも利用できるし、build
メソッドなどインスタンスを作るメソッドでも利用できる。
コールバック
コールバックとして、以下の4つがある。
after(:build)
: buildとcreateで実行before(:create)
: createで実行after(:create)
: createで実行after(:stub)
: build_stubbedで実行
after(:build, :create)
のように複数のコールバック対象を指定することが可能。