Rails + RSpec + factory_bot にて、あるモデルの関連データを生成する方法を調べたところ、factory_botのGETTING_STARTEDにいろいろな方法が記載されていました。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md
そこで、GETTING_STARTEDの内容を素振りしてみたときのメモを残します。
目次
- 環境
- テーブル構造
- 関連データを順次生成
- 多側のfactoryで、関連データを一度に生成 (Associations)
- Associationsを定義する場所について
- traitを使った生成
- ソースコード
環境
テーブル構造
前回の記事同様、 Area
と Apple
モデルを使います。
Area
class Area < ApplicationRecord has_many :apples end
area
に関連付けなくても良いとするため、
- 外部キーの
area_id
は nullable - Railsの
belongs_to
でoptional: true
とします。
class Apple < ApplicationRecord belongs_to :area, optional: true end
関連データを順次生成
各factoryを用意します。factoryの中では関連付けを定義しません。
# rspec/factories/apples.rb FactoryBot.define do factory :apple do end end # rspec/factories/area.rb FactoryBot.define do factory :area do name { '日本' } end end
多対一の一側(area)を生成し、その値を多側(apple)へ渡して生成します。
context '順次生成' do let(:area) { create(:area) } let(:apple) { create(:apple, area: area) } it do expect(Apple.find(apple.id).area).to eq(area) end end
多側のfactoryで、関連データを一度に生成 (Associations)
先ほど見たように、関連を順次生成する場合、必要な関連をletなどで定義しておく必要があります。
「一つのfactoryを実行したら、合わせて関連データも作成したい」場合を調べたところ、factory_botの Associations
という機能を使えば良さそうでした。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#associations
Associations
では
- 関連を使う
- factoryを使う
という方法があるため、それぞれ見ていきます。
関連を使った生成
関連を使う場合、
- 暗黙的定義
- 明示的定義
- インライン定義
の3パターンの指定方法があるため、それぞれ見ていきます。
暗黙的定義
一側のfactory名(area)と、多側のモデルにある関連名(belongs_to)が同じ場合、暗黙的な関連が使えます。
例えば
# 一側のfactory FactoryBot.define do factory :area do name { '日本' } end end # 多側のモデル class Apple < ApplicationRecord belongs_to :area, optional: true end
という関連の定義がモデルでされていたとします。
その場合、多側のfactoryで
FactoryBot.define do factory :apple do factory :apple_association_by_implicit do # Areaのfactory名(area)と、Appleモデルの関連名(belongs_to)が同じ場合、暗黙的な関連を使える area end end end
のように定義できます。
これにより、テストコードでは create
を1回使うだけで関連を生成できます。
context '暗黙的な定義' do before { create(:apple_association_by_implicit) } it do expect(Apple.last.area.name).to eq('日本') end end
明示的定義
関連を明示的に定義するには、 association
を使います。
factory :apple_association_by_explicit do association :area end
テストコードは暗黙的な書き方のときと同様です。
context '明示的な定義' do before { create(:apple_association_by_explicit) } it do expect(Apple.last.area.name).to eq('日本') end end
インライン定義
関連はインラインでも定義できます。
factory :apple_association_by_inline do area { association :area } end
テストコードでの使い方は、他の関連定義と同様です。
context 'インライン定義' do before { create(:apple_association_by_inline) } it do expect(Apple.last.area.name).to eq('日本') end end
factoryを使った生成
ここまでは関連を使った定義を見てきました。
ただ、関連の生成にはfactoryを使いたい場合があるかもしれません。
その場合、一側のfactoryを多側のfactoryに指定します。
例えば、一側であるAreaモデルのfactoryが以下のように定義されている場合に、多側であるAppleモデルのfactoryをどう定義すればよいか見ていきます。
# rspec/factories/areas.rb FactoryBot.define do factory :area do name { '日本' } factory :aomori_area do name { '青森県' } end factory :nagano_area do name { '長野県' } end factory :iwate_area do name { '岩手県' } end end end
暗黙的定義
関連 area
の後ろに factory
を指定します。
以下の例ではAreaモデルのfactory aomori_area
を指定しています。
# rspec/factories/apples.rb factory :apple_factory_by_implicit do area factory: :aomori_area end
これで、関連の生成に aomori_area
factoryが使われます。
context '暗黙的な定義' do before { create(:apple_factory_by_implicit) } it do expect(Apple.last.area.name).to eq('青森県') end end
明示的定義
明示的なfactory定義は、 association
に factory
を渡せばよいです。
以下の例ではAreaモデルのfactory nagano_area
を指定しています。
# rspec/factories/apples.rb factory :apple_factory_by_explicit do association :area, factory: :nagano_area end
使い方は暗黙的と同じです。
context '明示的な定義' do before { create(:apple_factory_by_explicit) } it do expect(Apple.last.area.name).to eq('長野県') end end
インライン定義
インライン定義の場合は、 association
にfactoryを指定します。
以下の例ではAreaモデルのfactory iwate_area
を指定しています。
# rspec/factories/apples.rb factory :apple_factory_by_inline do area { association :iwate_area } end
使い方も同様です。
context 'インライン定義' do before { create(:apple_factory_by_inline) } it do expect(Apple.last.area.name).to eq('岩手県') end end
一側のfactoryで定義された属性を上書き
ここまでは一側のfactoryの定義に従い、関連データを生成していました。
ただ、一側のfactoryで定義された属性を上書きして関連データを生成したいこともあります。
その場合は、一側のfactoryを定義するときに、上書きしたい属性も合わせて定義すれば良さそうです。
例えば、一側のAreaモデルのfactoryが以下のように定義されていたとします。
# rspec/factories/areas.rb factory :area do name { '日本' } end
このとき、 area
factoryで定義された属性 name
を上書きしたい場合は、以下のように name
を指定して上書きしたい属性の値を渡します。
# rspec/factories/apples.rb factory :apple_factory_with_overriding_attributes do association :area, factory: :area, name: '山形県' end
使い方は今までと同じです。
context '関連先のfactoryで定義している属性の上書き' do before { create(:apple_factory_with_overriding_attributes) } it do expect(Apple.last.area.name).to eq('山形県') end end
Associationsを定義する場所について
ここまで見てきた通り、factoryの中にAssociationsを定義することで関連データを生成できます。
ただ、
# rspec/factories/apples.rb FactoryBot.define do factory :apple do area # 関連を指定 end end
となっていた場合、create(:apple)
とするごとにareaが生成されます。
そのため、仮にareaが不要な場合は困ってしまうかもしれません。
そこで、
- factoryの継承
- trait
のいずれかの方法により、areaが不要な場合には生成しないよう定義できます。
factoryの継承
まずは、factoryの継承という機能を使って、必要に応じてareaを生成するようにしてみます。
ちなみに、factoryの継承について公式ドキュメントには
- parentを指定したfactory
- ネストしたfactory (sub factory)
の2種類の記載がありました。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#inheritance
それぞれ見ていきます。
parentを指定したfactory
apple
factoryと同階層に別のfactory apple_with_parent
を定義し、 parent
として親factoryである apple
を指定します。
また、別factoryの中で関連を指定します。
# rspec/factories/apples.rb FactoryBot.define do factory :apple do # ベースとなるfactoryでは関連を指定しない end # parentを指定したfactoryを用意し、関連を指定する factory :apple_with_parent, parent: :apple do area end end
これにより、
- areaが不要なときは
apple
factoryを使う - areaが必要なときは
apple_with_parent
factoryを使う
という方法が取れます。
context 'parent指定したfactoryを使う' do before { create(:apple_with_parent) } it do expect(Apple.last.area.name).to eq('日本') end end
ネストしたfactory
factory_botでは、factoryの中にfactoryをネストして定義することができます。
ネストしている場合、一階層上のfactoryの定義を継承できます。
例えば、 apple
factoryの中に apple_association_by_implicit
factoryを定義し、その中で関連を指定したとします。
FactoryBot.define do factory :apple do factory :apple_association_by_implicit do area end end end
これでも、areaが必要なときと不要なときでfactoryの使い分けができるようになります。
context '暗黙的な定義' do context '関連を生成する' do before { create(:apple_association_by_implicit) } it '関連が生成されていること' do expect(Apple.last.area.name).to eq('日本') end it 'その他の属性は継承されていること' do expect(Apple.last.name).to eq('秋映') end end context '関連を生成しない' do before { create(:apple) } it '関連が生成されていないこと' do expect(Apple.last.area.blank?).to eq(true) end it 'その他の属性は継承されていること' do expect(Apple.last.name).to eq('秋映') end end end
traitを使った生成
trait1つ使う
factory_botには trait
という、属性をグルーピングする機能があるため、これも試してみます。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#traits
まずfactoryに trait
を定義します。
FactoryBot.define do factory :apple do trait :aomori_apple do association :area, factory: :aomori_area end end end
create
時にtrait名のシンボルを渡すことで、 trait
で指定した属性の値がfactoryの生成時に使われるようになります。
以下では apple
factoryを使うときに aomori_apple
trait を使用して、関連 area
を生成しています。
context 'trait 1つ' do # trait `aomori_apple` を使う before { create(:apple, :aomori_apple) } it do expect(Apple.last.area.name).to eq('青森県') end end
traitを複数使ったときに優先されるtrait
factory_botにて生成する際、 trait
は複数指定できます。
例えば、以下のような同じ関連だけど別のnameを設定するtraitが複数あるとします。
FactoryBot.define do factory :apple do trait :aomori_apple do association :area, factory: :aomori_area end trait :hokkaido_apple do association :area, factory: :hokkaido_area end end end
これら複数のtraitを1回の生成で利用した場合、何が優先されるのかをみていきます。
create時にtraitを指定
まずは create
時にtraitを指定する場合です。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#using-traits
今回は、 aomori_apple > hokkaido_apple
の順で指定します。
create(:apple, :aomori_apple, :hokkaido_apple)
すると、後ろで指定したtrait (今回であれば hokkaido_apple
) の定義が利用されました。
context 'createで複数traitを指定' do before { create(:apple, :aomori_apple, :hokkaido_apple) } it do # 後ろのtraitが適用される expect(Apple.count).to eq(1) expect(Apple.last.area.name).to eq('北海道') end end
factoryの定義に traits を指定
factoryでtraitを使用する場合、 traits
にtrait名のシンボルを渡すことで複数のtraitを適用できます。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#defining-traits
factoryでtraitsを指定する場合も、先ほど同様 aomori_apple > hokkaido_apple
の順で指定します。
factory :multiple_traits, traits: %i[aomori_apple hokkaido_apple]
すると、この場合も後ろで定義したtrait (hokkaido_apple
) が利用されました。
context 'factoryで複数traitを指定' do before { create(:multiple_traits) } it do # 後ろのtraitが適用される expect(Apple.count).to eq(1) expect(Apple.last.area.name).to eq('北海道') end end
一側のfactoryで、関連データを生成(callback)
ここまでは多側のfactoryを使うときに一側の関連データも生成する例を見てきました。
一方、callbackという仕組みを使えば、一側のデータ生成時に多側の関連データを作成することもできます。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#callbacks
今回は一側である area
のfactoryにて、create後のcallback after(:create)
を使って関連データを生成してみます。
まずはfactoryを定義します。
factory :callback_area do name { '秋田県' } after(:create) { |area| create(:apple, area: area) } end
続いて、定義したfactoryを利用してみると、callbackで指定した内容で apple
と area
の関連が作成されています。
context 'factoryのcallbackで生成' do before { create(:callback_area) } it do expect(Apple.last.area.name).to eq('秋田県') end end
transientを利用した条件分岐による関連の生成
factory_botには transient
という、factory内でのみ利用できる属性を定義する機能があります。
https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#transient-attributes
transient
の使用例については、以下の記事が参考になりました。
- FactoryGirlのtransientとtraitを活用する - Qiita
- ruby on rails - What is the purpose of a
transient do
block in FactoryBot factories? - Stack Overflow
今回は transient
でフラグを定義し、フラグが true
であれば
- 関連を生成
- apple.nameの先頭に
シナノ
を追加する
という処理を行うcallbackを定義して試してみます。
factory :apple do trait :with_transient do transient do is_nagano { false } end after(:create) do |apple, evaluator| if evaluator.is_nagano # areaはoptionalなbelongs_toなので、後から追加可能 apple.area = create(:nagano_area) apple.name = "シナノ#{apple.name}" apple.save! end end end end
この trait
を使い、 transient
のキーを指定する/しないでどう変わるかを見ていきます。
transientのキーを引数で指定しない
create時に is_nagano
を指定しない場合は、callbackの内容は反映されません。
context '引数なし' do before { create(:apple, :with_transient, name: 'フジ',) } it do actual = Apple.last expect(actual.area).to eq(nil) expect(actual.name).to eq('フジ') end end
transientのキーを引数で指定する
一方、create時に is_nagano: true
を指定した場合は、callbackの内容が反映されます。
context '引数あり' do before { create(:apple, :with_transient, name: 'フジ', is_nagano: true) } it do actual = Apple.last expect(actual.area.name).to eq('長野県') expect(actual.name).to eq('シナノフジ') end end
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app
今回のプルリクはこちらです。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/7