FactoryBotベストプラクティス
has_many
関連の定義方法
FactoryBotの定義チートシート#has_manyアソシエーションを参照。
FactoryBotの定義ファイル内でcreate
やbuild
を使わずassociation
を使う
createは保存不要な場合でも常にDBに保存されてしまうし、buildを使った場合でもattributes_for
メソッドやbuild_stubbed
メソッドの挙動がおかしくなる。
FactoryBot.define do
factory :user do
# attributes_forの戻り値に
# buildメソッドの戻り値(つまりActiveRecordのインスタンス)
# が入ってきてしまう
# build_stubbedではこれらのインスタンスのIDなどは設定されない
blogs { build_list(:blog, 5) }
profile { build(:profile) }
end
end
したがって、FactoryBotの定義ファイル内で参照するならassociation
を使った方が良い。association
ならattributes_for
メソッドやbuild_stubbed
メソッドでも適切な結果になる。
FactoryBot.define do
factory :user do
blogs { Array.new(5) { association(:blog, user: instance) }.compact }
profile { association(:profile) }
end
end
また、rubocop-factory_bot
を入れているならFactoryBot/FactoryAssociationWithStrategyに警告される。
ディスクアクセスを最小化する
関連の定義もそうだが、ディスクアクセスが発生すれば、そのぶんパフォーマンスが低下することになる。したがって、ファイルからデータを読み込むようなfactoryは極力インスタンス化させないようにしなければならない。
FactoryBot.define do
factory :user do
trait :with_big_data do
big_data { File.read(Rails.root / 'spec/fixtures/files/user_big_data') }
end
end
end
上記のようにtrait
を使って、必要な場合にのみ参照させるか、factoryを分けてしまうなどの対応を取った方が良い。
trait
はテスト側から参照しない
テスト側からtrait
を参照しない方が良い。参照したとしても、レゴブロックのようにtrait
を組み合わせるのではなく、すでにあるfactoryをチューニングするスイッチとして使った方が良い。
例えば、以下のようなfactoryを考える。
FactoryBot.define do
factory :user do
login_name { 'account' }
premium { false }
trait :normal_user
trait :premium_user do
premium { true }
end
trait :user_has_blogs do
blogs { Array.new(blogs_count) { association(:blog, user: instance) }.compact }
end
end
end
このfactoryを利用するなら、create(:user, :premium_user, :user_has_blogs, login_name: 'suzuki')
のようになる。もし、user_has_blogs
がnormal_user
用のtraitでpremium_user
とuser_has_blogs
の組み合わせがデータ不整合を起こしていたとしても、テストが失敗しないかぎりは間違いに気づくことは難しいだろう。
このように、trait
をレゴブロックのように組み合わせる設計をしてしまうと、factoryを利用する側に組み合わせの整合性の責任を負わせることになる。そして、factoryはテストのあらゆる場所から利用されることを考えれば、まずい設計であることは明らかである。
したがって、整合性の責任はfactoryの定義内で負うべきである。factory :normal_user
やfactory :normal_user_has_blogs
のように整合性を持つ単位でfactoryを用意した方が良い。
trait
を定義するとしても、factoryから参照するだけでテスト側から参照しないようにするか、整合性に影響しないちょっとした設定を変更するスイッチとして利用した方が良い。
factoryを作る単位
DDD的に言えば、factory :foo { ... }
とした場合のfoo
はAggregateあるいはEntityの名前として、定義内容もAggregateを構築するのに必要なデータとそれらのデータをテーブルに保存するのに必要なデータを分けて記載するのが良いと考えている。
要はこういうことである。
# 履歴書テーブルかつ履歴書Aggregateのためのfactory
factory :resume do
# 履歴書データを保存するための前提となるアソシエーション
user { association(:user, resume: instance) }
# 履歴書Aggregateを構築するのに必要な情報
name { '鈴木太郎' }
address { '東京都渋谷区なんたら 1-2-3' }
academic_backgrounds { Array.new(5) { association(:academic_backgrounds, resume: instance) }.compact }
end
# 学歴テーブルかつ学歴Entityのためのfactory
factory :academic_backgrounds do
# 学歴データを保存するための前提となるアソシエーション
resume { association(:resume, academic_backgrounds: [instance]) }
# 学歴Entityを構築するのに必要な情報
entrance_date { '2020/04/01'.to_date }
graduation_date { '2024/03/31'.to_date }
school_name { 'どこそこ情報専門学校' }
end
Factory名に日本語を使う
Factory名には日本語を使える。変にわかりにくい長文英語を書くぐらいならこちらの方がわかりやすい可能性がある。
factory :履歴書, class: 'Resume' do
factory :大卒履歴書 do
# ...
end
factory :短大履歴書 do
# ...
end
end
これについては賛否あると思うが、常にシンボルでありRuboCopのNaming/AsciiIdentifiersに警告されない点、定義(factory
メソッド)も利用(build
メソッドなど)もFactoryBotであることが明らかで、git grep 'factory :ファクトリー名[ ,]' spec/factories/
で一意に特定できることからありではないかと考えている。Trait名についても同様である。
なお、私個人は日本語変数名、クラス名には忌避感を持っている側だが、それでもなおFactory名を日本語にするのはありではないかと考えている。
Factory名からModelが何かわかるようにする
たまにFactory名がトリッキーでなんのModelのインスタンスを作るのか、Factory名からわからない場合がある。ひどいと別のModelを作ると誤読するようなケースもある。
なので、明確にfactory :大卒履歴書
のように「履歴書」のファクトリであることがわかるようにするか、もっと直接的にfactory :大卒履歴書Resume
のようにしてしまう方法も考えられる。
データを用意するのではなくプロダクションコードで作る
これは思いつきレベルのアイディアで完全に詰め切れているわけではない。
create
メソッドの場合にしか使えないが、プロダクションコードを実行した結果を再現したデータを用意するよりもプロダクションコードによってデータを作らせた方が安全である。実行した結果を再現したデータというのはMockを使ったテストと似たような物で、テストの妥当性が再現したデータが正当かどうかに依存してしまっている。
なので、例えばユーザーが非公開にしたブログというデータを用意したいのであれば、
factory :blog do
factory :非公開ブログ do
status { 'private' }
end
end
とするよりも
factory :blog do
factory :非公開ブログ do
after(:create) do |blog|
blog.to_private
end
end
end
とした方がよい。ただ、いうまでもなくbuild
のケースに対応できておらず、次のようにしたくなるがafter(:build)
コールバックはcreate
メソッドを実行した場合にも実行されてしまうため目的を達成できない(FactoryBotで'private'
を設定した後にto_private
を実行してしまう)。
# これでは目的は達成できない
factory :blog do
factory :非公開ブログ do
after(:build) do |blog|
blog.status = 'private'
end
after(:create) do |blog|
blog.to_private
end
end
end
色々調べてみたが、コールバックが実行したのがbuild
なのかcreate
なのか区別する方法はなく、グローバルなtransientを定義することもできないため、スマートなやり方はなさそうである(instance_variable_get
で中に手を突っ込めばできそうではあるが、内部実装に依存した記述はするべきではない)。一番単純な回避方法はbuild
用factoryとcreate
用factoryを用意する方法である。
factory :blog do
factory :build非公開ブログ do
status { 'private' }
end
factory :create非公開ブログ do
after(:create) do |blog|
blog.to_private
end
end
end
あるいは、:biuld非公開ブログ
ではなく:非公開ブログ
として単体テストレベルではこちらを用いて、:create非公開ブログ
は結合テストレベルで用いることを意図して:integration非公開ブログ
というような名前にしても良いかもしれない。
欲を言えば、CIでattributes_for(:非公開ブログ)
とcreate(:integration非公開ブログ)
の結果が一致していることを検証できると良いが、アソシエーション先も変更するようなプロダクションコードの場合もあるので、なかなか難しいだろうと思う。
FactoryBotの定義ファイルを実行可能ドキュメントとする
ここまで書いた内容を実践すると、Aggregateで使うデータが明らかになるし、プロダクションコードを実行するようになれば、実際のデータが生じるまでの処理の流れが明らかになる。
ApplicationServiceを定義していて、それを呼び出すようにしていれば、それは実態に即したユースケース記述となりうる。
DDDなんぞやらんよと思っても、Aggregateを特定処理を実行する際に必要なデータの最小セットと捉えれば有用性は伝わるだろうと思う。