【rspec】FactoryBotのbelongs_to、has_many実装方法

バックエンド

サンプルコードとして、ProjectモデルとUserモデルを例にします。
モデルの実装は以下です。

class Project < ApplicationRecord
	has_many :users
end
class User < ApplicationRecord
	belongs_to :project
end

このように関連のあるモデルで、片方を作成すれば自動的にもう片方も作成したいときの方法を記載します。

belongs_to

基本形は以下です。

FactoryBot.define do
	factory :user do
		association :project
	end
end
FactoryBot.define do
	factory :project
end

ProjectとUserに対応するfactoryをそれぞれ作成し、User側のfactoryにassociation :projectと記載します。これで以下のようにテストデータを作成すると、

FactoryBot.create(:user) # UserとProjectを作成して保存

Userと一緒にProjectも作成されます。

以下のようにcreateではなくbuildの場合、

FactoryBot.build(:user) # UserとProjectを作成(保存されない)

UserとProjectはメモリ上に作成され保存はされません。
もちろんsaveすると両方DBに保存されます。

user = FactoryBot.build(:user)
user.save # UserとProjectの両方が保存される

このように親側であるUserの状態と連動するので便利です。

associationのオプション

associationにはオプションがいろいろあります。

# 基本形
association :project

# association :project の省略形。factory名と関連名が同じ場合にこう書ける。
project

# factoryを指定することも可能。
association :project, factory: :sub_project

# Projectの属性を上書きできる。
association :project, project_name: 'プロジェクトA'

associationの上書き

User作成時に自動で作成されるProjectとは別のProjectを使いたい場合、create時に属性の上書きをすることが可能です。

my_project = Project.create(name: '私のプロジェクト')
FactoryBot.create(:user, project: my_project)

associationが使えないとき

そもそもモデルにbelongs_toが定義されていないけど実際には関連があって一緒に作成したい場合など(そんな時ある?って感じですが私のプロジェクトではある…)
属性のブロックにはロジックも書けるので、こんなふうにすることもできます。

project_id { Project.create(name: 'my project').id }

たとえばProjectがマスタデータのようなもので、seedなどで事前にデータを用意できる場合、以下のようにIDを指定することもあります。

project_id { 1 }

# ID指定に抵抗がある場合は、定数にしておくと良いかも
project_id { ProjectData::MY_PROJECT_ID }

必要なすべてのデータをrspecの中で作成するのは大変なので、マスタデータは本番環境と同じデータをseedやfixtureで事前に作成しておくのがおすすめです。

belongs_to optional:true の場合

UserはProjectへの所属が必須ではないとき(project_idのnullを許可する場合)

class User < ApplicationRecord
	belongs_to :project, optional: true
end

以下のようにfactoryを分けることができます。

FactoryBot.define do
	factory :user do

		factory :user_with_project do
			association :project
		end
	end
end

FactoryBot.create(:user) # Projectは作成されない
FactoryBot.create(:user_with_project) # Projectも作成される

factoryをネストしていますが、これはfactoryの継承の書き方です。

続いて別の方法で、以下のようにtraitを使う方法もあります。

FactoryBot.define do
	factory :user

	trait :with_project do
		association :project
	end
end

FactoryBot.create(:user) # Projectは作成されない
FactoryBot.create(:user, :with_project) # Projectも作成される

traitを使った方法は便利ですが、
FactoryBot.create(:user, :with_project, :with_〇〇, :with_△△, :with_□□...)
のようにtraitだらけになりがちです。
factoryを使うからには、整合性の取れたデータを提供した方が良いと思うので、traitを好きに組み合わせてね〜という設計よりは、想定される整合性の取れた状態のfactoryを提供する方が良いのではないかと個人的には思います。

has_many

Projectを作成したときに所属するUserを一緒に作成する場合

FactoryBot.define do
	factory :project, traits: [:with_users] do

		trait :with_users do
			transient do
				users_count { 0 }
			end

			after(:build) do |project, evaluator|
				evaluator.users_count.each do
					project.users << FactoryBot.build(:user, project: project)
				end
			end
		end
	end
end

FactoryBot.create(:project) # Userが一緒に作成されない
FactoryBot.create(:project, users_count: 5) # Userが5件作成される

afterはfactoryのコールバックで、この場合にはprojectをbuildしたあとに呼び出されます。
users_countで指定した数分、Userを作成します。デフォルトではUserは作成しません。

多対多(has_many through)の場合

Projectは複数のCategoryに所属できるコードを例にします。
モデルの実装は以下です。

class Project < ApplicationRecord
	has_many :categories, through: project_categories
	has_many :project_categories
end
class ProjectCategory < ApplicationRecord
	belongs_to :project
	belongs_to :category
end
class Category < ApplicationRecord
end

先ほどと同じように、trait、transient、afterコールバックを利用して実装できます。

FactoryBot.define do
	factory :project, traits: [:with_categories] do

		trait :with_categories do
			transient do
				my_categories { [] }
			end

			after(:build) do |project, evaluator|
				evaluator.my_categories.each do |category_id|
					project.categories << FactoryBot.build(:project_category, category_id: category_id,  project_id: project.id)
				end
			end
		end
	end
end

FactoryBot.create(:project) # Categoryに所属なし
FactoryBot.create(:project, my_categories: [1, 2]) # Categoryにふたつ所属

この例ではCategoryのデータは事前にseedで登録済みですので、IDを直接しています。
所属するカテゴリをfactoryの呼び出し側が自由に設定できて便利です。もちろん、整合性を考慮して自由に設定させたくなければ、factory内で閉じた実装をすると良いと思います。

belongs_toとhas_manyの循環に注意

User側からもProject側からもお互いを作成するようにfactoryを書いた場合、片方を作成するとお互いを作成しつづけ、無限ループになってしまいます。
これを回避するには、factory内でFactoryBot.buildFactoryBot.createを利用せず、User.newUser.createを使う…などでしょうか。

私のプロジェクトの場合は、has_manyをfactoryで実装する場合は、シンプルな多対多関係のとき、上記の例だとProjectとCategoryのような関係のときだけにしています。カテゴリはチェックボックスでCategory側に実装がほぼなく、Project側がテストのメインだ、という場合です。

逆にUserやProjectのような、どちらにもしっかりと実装がある場合は、factoryでhas_manyを実装しないルールにしています。

こんな回避方法もあるようです。
https://qiita.com/roba4coding/items/849ff18c4f57d3c4aff8

FactoryBotにこだわる必要はない

とても便利なFactoryBotですが、一方でfactoryのassociationをどんどんだとって無駄にたくさんのデータを作成してしまうこともあります。最小限のデータでよいときは、FactoryBotを使わずにモデルの機能だけでデータを作成しても良いと思います。テストの局面に応じて適切に使い分けていきたいです。

参考サイト

factory_botのGETTING_STARTED – github

タイトルとURLをコピーしました