FactoryBotアンチパターン8選
これはなに
社内で共有した資料をほぼそのまま公開します。
僕が数年間(8年くらい? 共有してから月日がたち11年くらいになった?)FactoryBot(旧:FactoryGirl)を使ってきてアンチパターンだと思っているものを紹介します。
そもそもFactoryBotを使うのが……という話をしたい方は別のところでお願いします。
そのうちZennとかにまとめたい気持ちです、と言ってから3年弱くらい経ちましたが供養のために公開しておきます。
アンチパターン
パターン1. デフォルト値が固定値のfactory
例
FactoryBot.define do
factory :user do
name { "yuji_developer" }
end
end
問題点
Factoryのデフォルト値に依存したテストが書かれがちになります。
またテストに直接関係ない属性を書き換えがちになります。
解決策
Fakerを使うなり Time.current
などの動的な値が返るものを利用するなりして出来るだけランダム値を設定しましょう。
FactoryBot.define do
factory :user do
name { Faker::Name.name }
end
end
パターン2. 複数回利用できないfactory
例
class User
validates :email, uniqueness: true
end
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { "foo@example.com" }
end
end
問題点
複数回利用できない時点で問題ですが、こういうものがあるとテストに関係ない場合でもその属性を指定してテストが書かれたりします。
解決策
Fakerを使うなり sequence
を使うなりしましょう。
個人的には sequence
はRails consoleで開発用のデータを作るときに相性が悪いので避けています。
なお、Fakerを使う場合は unique
を使うと重複が発生しにくくなります。
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
end
end
パターン3. それ自身で完結していないfactory
他の値を指定しないと使えないパターンです。
例
class User
validates :name, presence: true
validates :email, presence: true
end
FactoryBot.define do
factory :user do
name { Faker::Name.name }
end
end
問題点
例では毎回 email
を指定して create(:user, email: "foo@example.com")
のようにする必要があります。
テストに関係ない属性でも毎回指定する必要があり使いにくいですしテストが読みにくくなります。
解決策
何も指定せずに create(:user)
などとして使えるようにしましょう。
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.unique.email }
end
end
パターン4. 矛盾したfactory
あり得ない値の組み合わせになってしまうパターンです。
例
難しい……。
問題点
使いにくいですしテストが通らなくなったりします。
解決策
あり得ない組み合わせにならないようにしましょう。
build
や build_stubbed
でDB書き込みしているfactory
パターン5. 例
build_stubbed
したのに関連が書き込まれてしまうパターン
class Post < ApplicationRecord
belongs_to :author, class_name: "User"
end
FactoryBot.define do
factory :post do
author { create(:user) }
end
end
問題点
build_stubbed
はDB書き込みをしないことでテスト実行速度を向上するために使われますがこの例のようなfactoryになっているとDB書き込みが行われてしまうためメリットを享受できません。
解決策
association
を使いましょう。
これによって build_stubbed(:post)
した場合は author
も build_stubbed
されますし、 create(:post)
した場合は author
も create
されます。
FactoryBot.define do
factory :post do
association :author, factory: :user
# または
# author factory: :user
end
end
パターン6. デフォルトで関連作りすぎのfactory
has_many
や has_one
関連のレコードがデフォルトで作られるパターンです。
例
class User < ApplicationRecord
has_many :posts, inverse_of: :author
end
class Post < ApplicationRecord
belongs_to :author, class_name: "User"
end
FactoryBot.define do
factory :user do
after(:create) do |user, _evaluator|
create_list(:posts, 10, author: user)
end
end
end
FactoryBot.define do
factory :post do
association :user, factory: :user
end
end
問題点
テストに不要なことが多いレコードが毎回作られてしまい速度劣化の元になります。
また has_many
や has_one
で作られる側のfactoryを使う際に意図しないレコードが作られるようになりがちです。例だと create(:post)
すると post
が11レコード出来てしまう)
解決策
必須の belongs_to
関連以外はデフォルトで作成されないようにします。
注意したいのは特定の状態の時にだけ作られる関連があったときにtraitで他の属性と同時に作ってしまいがちなことです。これも複数のtraitに分けると良いでしょう。(例: xxx
と xxx_with_yyy
など)
また、複数の関連を作る場合は数を指定可能にしておくと良いでしょう。
FactoryBot.define do
factory :user do
trait :with_posts do
transient do
post_count { 1 } # レコード数に意味があるような場合はランダム値にすることもあります
end
after(:create) do |user, _evaluator|
create_list(:posts, evaluator.post_count, author: user)
end
end
end
end
パターン7. レコードの状態を表すtraitや子のfactoryを提供していないfactory
例
難しい……。
問題点
特定のレコードの状態を作る場合に毎回値セットの組み合わせを指定するのは大変ですしfactoryを使うのが難しくなります。
解決策
traitやそれを組み合わせた子factoryを作りましょう。
パターン8. アプリケーションロジックに依存しているfactory
例
難しい……。
問題点
アプリケーションロジックの変更でfactoryが意図しない挙動をするようになったりします。
解決策
factoryのことはfactory内で完結するようにします。
factoryに限らずテストデータの準備にアプリケーションロジックを使うのは避けましょう。
僕が考えた最強のfactory作成方針
いまは違う気持ちのものもあるかもしれないけど書いた当時の内容そのまま出しておきます。
factory作成方針
-
factory
はそれ自身で完結し何も考えずにcreate
してレコードを作成できる - 各カラムのデフォルト値は出来るだけランダム値とする
- nullableなカラムの場合はランダム値の候補に
nil
を含める - 特定の値を設定したい場合は
trait
を利用する - 原則としてデフォルトで
active { true }
のようなことをしない - ただし削除フラグなど通常は切り替わらないようなものはデフォルトで固定値を設定するのを容認する
- nullableなカラムの場合はランダム値の候補に
- DBのカラムのデフォルト値にできるだけ頼らない
-
trait
はシンプルな状態を表す - テストに関係ないカラムを個別指定しなくても使える
factory
にする - ルートになる
factory
はそのレコードが作成された初期状態に近いものにする- ネストしていないかつ
parent
を指定していないfactory
- 通常はモデル名と同じ名前
- ネストしていないかつ
-
trait
を組み合わせて特定の状態を表す子factory
を用意する- それが必要ない程度にシンプルなモデルの場合は作成しなくても良い
- ルート
factory
はhas_one
/has_many
関連など必須のbelongs_to
関連以外をデフォルトで持たない- 関連を作る場合は
trait
を指定する
- 関連を作る場合は
- 作成される関連がデフォルトの状態で矛盾しないようにする
- A belongs to B, A belongs to C, B belongs to Cと関連がある場合にAからCとBからCの関連が矛盾しないようにする
-
build
しただけで関連がcreate
されないようにする- 関連作成時に
strategy
にcreate
を指定しない - テスト実行速度のために
build
/build_stubbed
を使いやすくする
- 関連作成時に
-
factory
内で出来るだけアプリケーションのロジックを呼び出さない - データの状態にパターンがある場合はルートの
factory
をそのまま使うのを避ける- データの状態に名前をつけた子の
factory
を作る- e.x. メンバー、管理者ユーザー、etc...
- 名前を付けることで指定するパラメーター数が減らなくても指定することでデータの状態が表せるという価値がある
- データの状態に名前をつけた子の
factoryとtraitの使い分け
factoryを使うと良いもの
- 性質が違うものを使い分ける場合
- メンバー、管理者ユーザーを使い分けるようなシーン
- 複数の
trait
を組み合わせて特定の状態を作る場合-
trait
の組み合わせのショートカット的な位置づけ
-
traitを使うと良いもの
- シンプルな状態を表す場合
- 決済成功した注文、決済失敗した注文
- 特定のカラムの値を固定値にする場合
- メルマガ受信設定オン、メルマガ受信設定オフ
命名規則
factory
- ルート
factory
はモデル名と合わせる - 子
factory
にはモデル名を含めると良い
# ルートfactory
factory :user do
# 子factory
factory :user_xxx, traits: %i[with_credit_card with_address]
end
trait
-
with_xxx
は別レコードを作る場合など自身のレコード以外を作成する場合に使う- 例:
:with_credit_card
- 特定のカラムに値を設定するだけの場合には使わない
- 例: ステータス値を設定するなど
- 例:
-
:<状態名>
- 状態を表す
trait
に使う- 例:
:active
- 例:
- 設定されるのは1カラムとは限らない
- 状態を表す
-
:<カラム名>_<設定値>
- 特にenum値を設定する
trait
に使う- 例:
:status_active
- 例:
- 特にenum値を設定する
# 別レコードを作るtrait
trait :with_credit_card do
credit_card do
association(:credit_card, user: instance)
end
end
# 特定のカラムに値を設定するためのtrait
trait :status_active do
active { true }
end
# 状態を表すtrait
trait :active do
active { true }
inactive_at { nil }
end
参考資料
- factory_bot/GETTING_STARTED.md at main · thoughtbot/factory_bot
- Rails アンチパターン - 錆びついたファクトリー (factory_girl) - アジャイルSEの憂鬱
- willnet/rspec-style-guide: 可読性の高いテストコードを書くためのお作法集
まとめ
がんばれ!
Discussion