FactoryBot和訳
ソース(英語)はこちらです。
素晴らしい解説を作ってくださった方々にこの場を借りて感謝申し上げます。
セットアップ
Gemfileをアップデート
Railsで使う場合:
gem "factory_bot_rails"
Rails以外で使う場合:
gem "factory_bot"
テストスイートを構成する
RSpec
Railsを使用している場合は, spec/support/factory_bot.rb以下の設定を追加し,
rails_helper.rbでそのファイルを必ず要求してください:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Railsを使用していない場合:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.before(:suite) do
FactoryBot.find_definitions
end
end
Test::Unit
class Test::Unit::TestCase
include FactoryBot::Syntax::Methods
end
Cucumber
# env.rb (Rails example location - RAILS_ROOT/features/support/env.rb)
World(FactoryBot::Syntax::Methods)
Spinach
class Spinach::FeatureSteps
include FactoryBot::Syntax::Methods
end
Minitest
class Minitest::Unit::TestCase
include FactoryBot::Syntax::Methods
end
Minitest::Spec
class Minitest::Spec
include FactoryBot::Syntax::Methods
end
minitest-rails
class ActiveSupport::TestCase
include FactoryBot::Syntax::Methods
end
テストスイートにFactoryBot::Syntax::Methodsを含めない場合,
すべてfactory_botメソッドの前にFactoryBotを付ける必要があります.
factoryの定義
Factory名と属性
各factoryには, 名前と一連の属性があります.
この名前からクラスがデフォルトで設定されています:
# Userクラスと推測
FactoryBot.define do
factory :user do
first_name { "John" }
last_name { "Doe" }
admin { false }
end
end
クラスを明示的に指定する
明示的にクラスを指定することも可能です:
# User クラスを使用 (そうでなければ, Adminは推測されていただろう)
factory :admin, class: "User"
定数が利用可能な場合は定数も渡すことができます(定数を参照するとイーガーリーロードされるため, 大規模なRailsアプリケーションではテストパフォーマンスに問題が生じる可能性があることに注意してください).
factory :access_token, class: User
ハッシュ属性
Rubyのブロック構文では, 属性をHashで定義する場合(例: シリアライズ/JSONカラムの場合), 2組の中括弧が必要です:
factory :program do
configuration { { auto_resolve: false, auto_define: true } }
end
一番良いやり方
各クラスには, そのクラスのインスタンスを作成するために必要な最も単純な属性のセットを提供するfactoryを一つ用意することをお勧めします. ActiveRecordのオブジェクトを作成する場合は, 検証で必要とされる属性とデフォルトを持たない属性のみを提供する必要があることを意味します. その他のfactoryは継承によって作成し, 各クラスの一般的なシナリオをカバーすることができます.
同じ名前のfactoryを複数定義しようとすると, エラーが発生します.
ファイルパスの定義
factoryはどこでも定義できますが, 以下の場所のファイルにfactoryが定義されている場合は, FactoryBot.find_definitionsを呼び出した後に自動的にロードされます:
test/factories.rb
spec/factories.rb
test/factories/*.rb
spec/factories/*.rb
静的な属性
factory_bot 5では, 静的属性(ブロックなし)は使用できなくなりました.
削除の決定については, こちらのブログ記事で詳しく説明しています.
factoryの使い方
ビルドストラテジー
factory_botは, いくつかの異なるビルド戦略をサポートしています:
build, create, attributes_for, build_stubbed があります:
ストラテジーとは, 戦略のこと. 実行の優先順位を定める.
# 保存されていないUserインスタンスを返す
user = build(:user)
# 保存されたUserインスタンスを返す
user = create(:user)
# Userインスタンスを構築するために使用できる属性のハッシュを返す
attrs = attributes_for(:user)
# 定義されたすべての属性が無効化されたオブジェクトを返す
stub = build_stubbed(:user)
# 上記のメソッドのいずれかにブロックを渡すと, 戻りオブジェクトが生成される
create(:user) do |user|
user.posts.create(attributes_for(:post))
end
属性のオーバーライド
どのストラテジーを使っても, ハッシュを渡すことで定義された属性をオーバーライドすることが可能です:
# Build a User instance and override the first_name property
user = build(:user, first_name: "Joe")
user.first_name
# => "Joe"
build_stubbed と Marshal.dump
build_stubbed で作成されたオブジェクトは Marshal.dump でシリアライズできないことに注意してください. なぜなら factory_bot はこれらのオブジェクトに対して singleton メソッドを定義しているからです.
エイリアス
factory_bot を使うと, 既存のfactoryにエイリアスを定義して, 再利用しやすくすることができます. 例えば, Postオブジェクトにauthor属性があり, それが実際にはUserクラスのインスタンスを参照している場合, これは便利です. 通常 factory_bot はアソシエーション名からfactory名を推測しますが, この場合, 無駄に author factoryを探すことになります. そこで, User factoryのエイリアスを作成し, 別名で使用できるようにします.
factory :user, aliases: [:author, :commenter] do
first_name { "John" }
last_name { "Doe" }
date_of_birth { 18.years.ago }
end
factory :post do
# エイリアスを使用すると, 関連付けの代わりにauthorを記述できます
# :author, factory::user
author
title { "How to read a book effectively" }
body { "There are five steps involved." }
end
factory :comment do
# The alias allows us to write commenter instead of
# association :commenter, factory: :user
commenter
body { "Great article!" }
end
従属属性
属性は, 動的属性ブロックに生成されるエバリュエータを使用して, 他の属性の値を基準とすることができます:
factory :user do
first_name { "Joe" }
last_name { "Blow" }
email { "#{first_name}.#{last_name}@example.com".downcase }
end
create(:user, last_name: "Doe").email
# => "joe.doe@example.com"
トランジェント属性
トランジェント属性とは, factory定義内でのみ利用可能な属性で, ビルドされるオブジェクトには設定されない. これにより, factory内部でより複雑なロジックを実現することができます.
他の属性
factoryに一時的な属性を渡すことで, コードをDRY原則に従った記述にすることができる場合があります. トランジェント属性は, 他の属性の中でアクセスすることができます. (参考従属属性):
DRY原則とは、すべての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならないという原則。 参考リンク(Qiita)
factory :user do
transient do
rockstar { true }
end
name { "John Doe#{" - Rockstar" if rockstar}" }
end
create(:user).name
#=> "John Doe - ROCKSTAR"
create(:user, rockstar: false).name
#=> "John Doe"
attributes_for
一時的な属性は attributes_for 内で無視され, その属性が存在したり, それを上書きしようとしたりしても, モデルには設定されません.
コールバック
factory_bot コールバックでエバリュエータにアクセスする必要がある場合は, (エバリュエーター用の)2番目のブロック引数を宣言して, そこからtransient属性にアクセスする必要があります.
factory :user do
transient do
upcased { false }
end
name { "John Doe" }
after(:create) do |user, evaluator|
user.name.upcase! if evaluator.upcased
end
end
create(:user).name
#=> "John Doe"
create(:user, upcased: true).name
#=> "JOHN DOE"
関連付け
一時的な関連付けは factory_bot ではサポートされていません. トランジェントブロック内のアソシエーションは, 通常の非トランジェントなアソシエーションとして扱われます.
必要であれば, 一時的な属性内にfactoryを構築することで, 一般にこれを回避することができます:
factory :post
factory :user do
transient do
post { build(:post) }
end
end
メソッド名 / 予約語属性
もし属性が既存のメソッドや予約語(DefinitionProxy class)のすべてのメソッド)と衝突する場合は, add_attributeで定義することができます.
factory :dna do
add_attribute(:sequence) { 'GATTACA' }
end
factory :payment do
add_attribute(:method) { 'paypal' }
end
継承
入れ子されたfactory
factoryを入れ子にすることで, 共通の属性を繰り返すことなく, 同じクラスに対して複数のfactoryを簡単に作成することができます:
factory :post do
title { "A title" }
factory :approved_post do
approved { true }
end
end
approved_post = create(:approved_post)
approved_post.title # => "A title"
approved_post.approved # => true
親を明示的に割り当てる
親を明示的に割り当てることもできます:
factory :post do
title { "A title" }
end
factory :approved_post, parent: :post do
approved { true }
end
一番良いやり方
前述したように, 各クラスには作成に必要な属性のみを持つ基本的なfactoryを定義するのがよい方法です. そして, この基本的な親を継承した, より特殊なfactoryを作成します. factoryの定義はコードなので, DRY原則に従った記述を保つようにしましょう.
関連付け
暗黙の定義
factory内に関連付けを作成することができます.
factory名がassociation名と同じ場合, factory名を省略できます.
factory :post do
# ...
author
end
明示的な定義
関連付けを明示的に定義できます. これは特に次の場合に便利です.
属性をオーバーライド
factory :post do
# ...
association :author
end
インライン定義
通常の属性内でインラインで関連付けを定義することもできますが, attributes_forストラテジーを使用する場合は, 値がnilになることに注意してください.
factory :post do
# ...
author { association :author }
end
factoryの指定
別のfactoryを指定することも可能です(ただし, ここではエイリアス[#エイリアス]も役立つかもしれません).
暗黙的:
factory :post do
# ...
author factory: :user
end
明示的:
factory :post do
# ...
association :author, factory: :user
end
インライン:
factory :post do
# ...
author { association :user }
end
属性のオーバーライド
属性を上書きすることもできます
暗黙的:
factory :post do
# ...
author factory: :author, last_name: "Writely"
end
明示的:
factory :post do
# ...
association :author, last_name: "Writely"
end
また, factoryからの属性を使用したインライン:
factory :post do
# ...
author_last_name { "Writely" }
author { association :author, last_name: author_last_name }
end
関連付けのオーバーライド
属性のオーバーライドを使用して, 関連するオブジェクトをリンクさせることができます:
FactoryBot.define do
factory :author do
name { 'Taylor' }
end
factory :post do
author
end
end
eunji = build(:author, name: 'Eunji')
post = build(:post, author: eunji)
ビルドストラテジー
factory_bot 5 以降, 関連付けは親オブジェクトと同じビルドストラテジーをデフォルトで使用するようになりました:
FactoryBot.define do
factory :author
factory :post do
author
end
end
post = build(:post)
post.new_record? # => true
post.author.new_record? # => true
post = create(:post)
post.new_record? # => false
post.author.new_record? # => false
これは, 以前のバージョンの factory_bot のデフォルトの動作とは異なり, 関連付けストラテジーが常に親オブジェクトのストラテジーと一致するわけではありません. もし古い動作を使い続けたい場合は, use_parent_strategy設定オプションをfalseに設定することができます.
FactoryBot.use_parent_strategy = false
# UserとPostの構築と保存
post = create(:post)
post.new_record? # => false
post.author.new_record? # => false
# ユーザーを構築して保存し, ポストを構築するが保存しない
post = build(:post)
post.new_record? # => true
post.author.new_record? # => false
関連するオブジェクトを保存しない場合は, factoryでstrategy::buildを指定します:
FactoryBot.use_parent_strategy = false
factory :post do
# ...
association :author, factory: :user, strategy: :build
end
# Userを作成し, Postを作成するが, どちらも保存しない
post = build(:post)
post.new_record? # => true
post.author.new_record? # => true
strategy: :buildオプションは, associationの明示的な呼び出しに渡す必要があり, 暗黙的なassociationでは使用できないことに注意してください:
factory :post do
# ...
author strategy: :build # <<< これは動作しません; author_idをnilにします
has_many関連付け
has_many関係性を持つデータを生成する方法はいくつかあります.
最も簡単なアプローチは, プレーンなRubyでヘルパーメソッドを記述して, さまざまなレコードを結び付けることです:
FactoryBot.define do
factory :post do
title { "Through the Looking Glass" }
user
end
factory :user do
name { "Rachel Sanchez" }
end
end
def user_with_posts(posts_count: 5)
FactoryBot.create(:user) do |user|
FactoryBot.create_list(:post, posts_count, user: user)
end
end
create(:user).posts.length # 0
user_with_posts.posts.length # 5
user_with_posts(posts_count: 15).posts.length # 15
オブジェクトの作成をfactory_bot内に完全に保持したい場合は, after(:create)コールバックで投稿を作成できます.
FactoryBot.define do
factory :post do
title { "Through the Looking Glass" }
user
end
factory :user do
name { "John Doe" }
# user_with_postsは, ユーザーが作成された後に投稿データを作成します
factory :user_with_posts do
# posts_countは, エバリュエーターを介したコールバックで使用可能な一時属性として宣言されます
transient do
posts_count { 5 }
end
# after(:create)は2つの値を生成します.
# ユーザーインスタンス自体と, 一時的な属性を含む, factoryからのすべての値を格納する評価.
# `create_list`の2番目の引数は作成するレコードの数であり, ユーザーが投稿に適切に関連付けられていることを確認します
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
# アプリケーションによっては, ここでレコードを再読み込みする必要があります
user.reload
end
end
end
end
create(:user).posts.length # 0
create(:user_with_posts).posts.length # 5
create(:user_with_posts, posts_count: 15).posts.length # 15
また, build, build_stubbed, createで動作するソリューションとして(attributes_forではうまく動作しませんが), インラインの関連付けを使用することができます:
FactoryBot.define do
factory :post do
title { "Through the Looking Glass" }
user
end
factory :user do
name { "Taylor Kim" }
factory :user_with_posts do
posts { [association(:post)] }
end
end
end
create(:user).posts.length # 0
create(:user_with_posts).posts.length # 1
build(:user_with_posts).posts.length # 1
build_stubbed(:user_with_posts).posts.length # 1
柔軟性を高めるために, これをコールバックの例のposts_count一時属性と組み合わせることができます:
FactoryBot.define do
factory :post do
title { "Through the Looking Glass" }
user
end
factory :user do
name { "Adiza Kumato" }
factory :user_with_posts do
transient do
posts_count { 5 }
end
posts do
Array.new(posts_count) { association(:post) }
end
end
end
end
create(:user_with_posts).posts.length # 5
create(:user_with_posts, posts_count: 15).posts.length # 15
build(:user_with_posts, posts_count: 15).posts.length # 15
build_stubbed(:user_with_posts, posts_count: 15).posts.length # 15
has_and_belongs_to_many関連付け
has_and_belongs_to_many関係性をもつデータの生成は, 上記のhas_many関連付けと非常に似ていますが, わずかな変更があります:
単一のオブジェクトを単一バージョンの属性名に渡すのではなく, オブジェクトの配列をモデルの複数形の属性名に渡す必要があります.
def profile_with_languages(languages_count: 2)
FactoryBot.create(:profile) do |profile|
FactoryBot.create_list(:language, languages_count, profiles: [profile])
end
end
またはコールバックアプローチで:
factory :profile_with_languages do
transient do
languages_count { 2 }
end
after(:create) do |profile, evaluator|
create_list(:language, evaluator.languages_count, profiles: [profile])
profile.reload
end
end
または, インラインで関連付けを行う方法です. (ここでは, 構築中のプロファイルを参照するためにinstanceメソッドを使用していることに注意してください):
factory :profile_with_languages do
transient do
languages_count { 2 }
end
languages do
Array.new(languages_count) do
association(:language, profiles: [instance])
end
end
end
ポリモーフィック関連
ポリモーフィック関連は, 次のtraitで処理できます:
FactoryBot.define do
factory :video
factory :photo
factory :comment do
for_photo # default to the :for_photo trait if none is specified
trait :for_video do
association :commentable, factory: :video
end
trait :for_photo do
association :commentable, factory: :photo
end
end
end
これにより:
create(:comment)
create(:comment, :for_video)
create(:comment, :for_photo)
相互接続されたアソシエーション
オブジェクトの相互関係は無限にあり, factory_botがその関係を処理するのに適しているとは限りません. 場合によっては, factory_botを使って個々のオブジェクトを構築し, それらのオブジェクトを結びつけるヘルパーメソッドをRubyで記述することが理にかなっていることもあります.
とはいえ, より複雑で相互に関連するいくつかの関係は, 構築中のinstanceを参照しながらインラインの関連付けを使用してfactory_botで構築することができます.
あなたのモデルがこのようなもので, 関連するStudentとProfileが両方とも同じSchoolに属しているとしましょう:
class Student < ApplicationRecord
belongs_to :school
has_one :profile
end
class Profile < ApplicationRecord
belongs_to :school
belongs_to :student
end
class School < ApplicationRecord
has_many :students
has_many :profiles
end
次のようなfactoryで, 生徒とプロファイルが互いに, そして同じ学校に接続されていることを確認することができます:
FactoryBot.define do
factory :student do
school
profile { association :profile, student: instance, school: school }
end
factory :profile do
school
student { association :student, profile: instance, school: school }
end
factory :school
end
この方法は, build, build_stubbed, createで動作しますが, attributes_forを使用すると, 関連付けはnilを返すことに注意してください.
また, initialize_withの中で属性を指定した場合(例: initialize_with { new(**attributes) }), その属性はnilになるので, instanceを参照してはいけないことに注意しましょう.
シーケンス
グローバルシーケンス
特定のフォーマットで一意な値(例えば, 電子メールアドレス)を作成することができます.
シーケンスは定義ブロックの中でsequenceを呼び出すことで定義され, シーケンスの中の値はgenerateを呼び出すことで生成されます:
# 新しいシーケンスの定義
FactoryBot.define do
sequence :email do |n|
"person#{n}@example.com"
end
end
generate :email
# => "person1@example.com"
generate :email
# => "person2@example.com"
動的な属性と
シーケンスは, 動的な属性で使用することができます:
factory :invite do
invitee { generate(:email) }
end
暗黙の属性として
または, 暗黙の属性として:
factory :user do
email # `email { generate(:email) }` と同じ
end
シーケンスを暗黙の属性として定義しても, シーケンスと同じ名前のfactoryがある場合は機能しないことに注意してください.
インラインシーケンス
特定のfactoryでのみ使用されるインラインシーケンスを定義することも可能です:
factory :user do
sequence(:email) { |n| "person#{n}@example.com" }
end
初期値
初期値をオーバーライドすることができます. #nextメソッドに反応する値であれば, どのような値でも動作します (e.g. 1, 2, 3, 'a', 'b', 'c')
factory :user do
sequence(:email, 1000) { |n| "person#{n}@example.com" }
end
ブロックなし
ブロックがなければ, 値は初期値から始まり, それ自体で増加します:
factory :post do
sequence(:position)
end
シーケンスの値は, #nextに応答する限り, 任意のEnumerableインスタンスである可能性があることに注意してください:
factory :task do
sequence :priority, %i[low medium high urgent].cycle
end
エイリアス
シーケンスにはエイリアスを含めることもできます. シーケンスエイリアスは同じカウンターを共有します:
factory :user do
sequence(:email, 1000, aliases: [:sender, :receiver]) { |n| "person#{n}@example.com" }
end
# :sender と :receiver が共有する :email の値カウンタを増加させます
generate(:sender)
エイリアスを定義し, カウンタにデフォルト値(1)を使用します
factory :user do
sequence(:email, aliases: [:sender, :receiver]) { |n| "person#{n}@example.com" }
end
値を設定する:
factory :user do
sequence(:email, 'a', aliases: [:sender, :receiver]) { |n| "person#{n}@example.com" }
end
値は, #next メソッドに対応していればよい. ここでは, 次の値は'a', 次に'b'などとなる.
巻き戻し
シーケンスはFactoryBot.rewind_sequencesで巻き戻しすることも可能です:
sequence(:email) {|n| "person#{n}@example.com" }
generate(:email) # "person1@example.com"
generate(:email) # "person2@example.com"
generate(:email) # "person3@example.com"
FactoryBot.rewind_sequences
generate(:email) # "person1@example.com"
登録されているすべてのシーケンスを巻き戻すことができます.
独自性
一意性制約を扱う場合, 生成されるシーケンス値と衝突するようなオーバーライド値を渡さないように注意してください.
この例では, emailは両方のユーザーで同じになります. emailが一意でなければならない場合, このコードはエラーになります:
factory :user do
sequence(:email) { |n| "person#{n}@example.com" }
end
FactoryBot.create(:user, email: "person1@example.com")
FactoryBot.create(:user)
trait
traitの定義
traitは, 属性をグループ化し, 任意のfactoryに適用することができます.
factory :user, aliases: [:author]
factory :story do
title { "My awesome story" }
author
trait :published do
published { true }
end
trait :unpublished do
published { false }
end
trait :week_long_publishing do
start_at { 1.week.ago }
end_at { Time.now }
end
trait :month_long_publishing do
start_at { 1.month.ago }
end_at { Time.now }
end
factory :week_long_published_story, traits: [:published, :week_long_publishing]
factory :month_long_published_story, traits: [:published, :month_long_publishing]
factory :week_long_unpublished_story, traits: [:unpublished, :week_long_publishing]
factory :month_long_unpublished_story, traits: [:unpublished, :month_long_publishing]
end
暗黙の属性として
traitは暗黙の属性として使用することができます:
factory :week_long_published_story_with_title, parent: :story do
published
week_long_publishing
title { "Publishing that was started at #{start_at}" }
end
なお, 暗黙の属性としてtraitを定義しても, traitと同じ名前のfactoryやシーケンスがある場合は機能しません.
属性の優先順位
同じ属性を定義するtraitは, AttributeDefinitionErrorsを発生させません. 最新の属性を定義するtraitが優先されます.
factory :user do
name { "Friendly User" }
login { name }
trait :active do
name { "John Doe" }
status { :active }
login { "#{name} (active)" }
end
trait :inactive do
name { "Jane Doe" }
status { :inactive }
login { "#{name} (inactive)" }
end
trait :admin do
admin { true }
login { "admin-#{name}" }
end
factory :active_admin, traits: [:active, :admin] # login will be "admin-John Doe"
factory :inactive_admin, traits: [:admin, :inactive] # login will be "Jane Doe (inactive)"
end
factoryの子要素
factoryの子では, 特質が付与する個々の属性をオーバーライドすることができます:
factory :user do
name { "Friendly User" }
login { name }
trait :active do
name { "John Doe" }
status { :active }
login { "#{name} (M)" }
end
factory :brandon do
active
name { "Brandon" }
end
end
traitの使用
また, factory_botからインスタンスを構築する際に, シンボルのリストとしてtraitを渡すこともできます.
factory :user do
name { "Friendly User" }
trait :active do
name { "John Doe" }
status { :active }
end
trait :admin do
admin { true }
end
end
# アクティブなステータスと名前 "Jon Snow "を持つ管理者を作成します
create(:user, :admin, :active, name: "Jon Snow")
この機能は, build, build_stubbed, attributes_for, およびcreateで動作します.
create_list と build_list メソッドもサポートされています. このファイルの "複数のレコードの構築と作成" のセクションで説明したように, 作成/構築するインスタンスの数を第2パラメータとして渡すことを忘れないようにしてください.
factory :user do
name { "Friendly User" }
trait :admin do
admin { true }
end
end
# 管理者を3人作成, ステータスは :active, 名前は "Jon Snow"
create_list(:user, 3, :admin, :active, name: "Jon Snow")
関連付け
traitは関連付けでも簡単に使用できます:
factory :user do
name { "Friendly User" }
trait :admin do
admin { true }
end
end
factory :post do
association :user, :admin, name: 'John Doe'
end
# "John Doe"という名前の管理者を作成します
create(:post).user
factoryとは異なる関連付け名を使用する場合:
factory :user do
name { "Friendly User" }
trait :admin do
admin { true }
end
end
factory :post do
association :author, :admin, factory: :user, name: 'John Doe'
# or
association :author, factory: [:user, :admin], name: 'John Doe'
end
# "John Doe"という名前の管理者を作成します
create(:post).author
trait の中の trait
traitは, 他のtraitの中で使用することで, その属性を混ぜることができます.
factory :order do
trait :completed do
completed_at { 3.days.ago }
end
trait :refunded do
completed
refunded_at { 1.day.ago }
end
end
一時的な属性
最後に, traitは一時的な属性を受け入れることができます.
factory :invoice do
trait :with_amount do
transient do
amount { 1 }
end
after(:create) do |invoice, evaluator|
create :line_item, invoice: invoice, amount: evaluator.amount
end
end
end
create :invoice, :with_amount, amount: 2
列挙型のtrait
enum属性を持つActive Recordモデルがある場合:
class Task < ActiveRecord::Base
enum status: {queued: 0, started: 1, finished: 2}
end
factory_bot は, enum の取り得る値ごとに自動的に特徴を定義します:
FactoryBot.define do
factory :task
end
FactoryBot.build(:task, :queued)
FactoryBot.build(:task, :started)
FactoryBot.build(:task, :finished)
traitを手動で書き出すのは面倒なので, 必要ない:
FactoryBot.define do
factory :task do
trait :queued do
status { :queued }
end
trait :started do
status { :started }
end
trait :finished do
status { :finished }
end
end
end
factoryごとにenum属性のtraitを自動定義したくない場合は, FactoryBot.automatically_define_enum_traits = falseを設定して機能を無効化することが可能です.
その場合でも, 特定のfactoryでenum属性のtraitを明示的に定義することは可能です:
FactoryBot.automatically_define_enum_traits = false
FactoryBot.define do
factory :task do
traits_for_enum(:status)
end
end
また, 特にActive Recordのenum属性に縛られることなく, 他の列挙可能な値にもこの機能を使用することが可能です.
配列で:
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, ["queued", "started", "finished"])
end
end
ハッシュで:
class Task
attr_accessor :status
end
FactoryBot.define do
factory :task do
traits_for_enum(:status, { queued: 0, started: 1, finished: 2 })
end
end
コールバック
デフォルトのコールバック
factory_botは, コードを挿入するための4つのコールバックを提供しています:
- after(:build) - factoryが作られた後に呼び出される (例:
FactoryBot.build,FactoryBot.create) - before(:create) - factoryが保存される前に呼び出される (例:
FactoryBot.create) - after(:create) - factoryが保存された後に呼び出される (例:
FactoryBot.create) - after(:stub) - factoryで代用の値が用意された後に呼び出される (例:
FactoryBot.build_stubbed)
例:
# ビルド後に generate_hashed_password メソッドを呼び出すfactoryを定義する
factory :user do
after(:build) { |user| generate_hashed_password(user) }
end
ブロックの中にユーザーのインスタンスがあることに注意してください. これは便利です.
複数のコールバック
また, 同じfactoryに複数の種類のコールバックを定義することも可能です:
factory :user do
after(:build) { |user| do_something_to(user) }
after(:create) { |user| do_something_else_to(user) }
end
factoryは, 同じ種類のコールバックをいくつでも定義することができます. これらのコールバックは指定された順番に実行されます:
factory :user do
after(:create) { this_runs_first }
after(:create) { then_this }
end
create を呼び出すと, after_build と after_create の両方のコールバックが呼び出されます.
また, 標準の属性と同様に, 子要素のfactoryは親要素のfactoryからコールバックを継承します(定義も可能).
これは, 同じコードを実行する様々なストラテジーを構築する際に便利です(すべてのストラテジーで共有されるコールバックが存在しないため).
factory :user do
callback(:after_stub, :before_create) { do_something }
after(:stub, :create) { do_something_else }
before(:create, :custom) { do_a_third_thing }
end
グローバルコールバック
すべてのfactoryのコールバックをオーバーライドするには, FactoryBot.defineブロック内で定義します:
FactoryBot.define do
after(:build) { |object| puts "Built #{object}" }
after(:create) { |object| AuditLog.create(attrs: object.attributes) }
factory :user do
name { "John Doe" }
end
end
Symbol#to_proc
Symbol#to_procに依存するコールバックを呼び出すことができます:
# app/models/user.rb
class User < ActiveRecord::Base
def confirm!
# confirm the user account
end
end
# spec/factories.rb
FactoryBot.define do
factory :user do
after :create, &:confirm!
end
end
create(:user) # creates the user and confirms it
factoryの変更
一連のfactory(gem開発者等から)が与えられたが, アプリケーションにより良く適合するようにそれらを変更したい場合は, 子要素のfactoryを作成してそこに属性を追加する代わりに, そのfacrtoryを変更できます.
gemが提供するUser factoryを利用する場合:
FactoryBot.define do
factory :user do
full_name { "John Doe" }
sequence(:username) { |n| "user#{n}" }
password { "password" }
end
end
属性を追加した子要素のfactoryを作るのではなく, その子要素のfactoryを作る:
FactoryBot.define do
factory :application_user, parent: :user do
full_name { "Jane Doe" }
date_of_birth { 21.years.ago }
health { 90 }
end
end
代わりにそのfactoryを変更することができます.
FactoryBot.modify do
factory :user do
full_name { "Jane Doe" }
date_of_birth { 21.years.ago }
health { 90 }
end
end
factoryを変更する場合, (コールバック以外は)好きな属性を変更することができます.
FactoryBot.modifyはfactoryに対する操作が異なるため, FactoryBot.defineブロックの外側で呼び出す必要があります.
警告: 変更できるのはfactoryのみであり(シーケンスやtraitは不可), コールバックは通常どおりに複合されます. したがって, 変更するfactoryがafter(:create)コールバックを定義している場合,after(:create)を定義してもそれはオーバーライドされず, 最初のコールバックの後に実行されるだけです.
複数のレコードを構築・作成する
時々, 一度に複数のfactoryのインスタンスを作成または構築したい場合があります.
built_users = build_list(:user, 25)
created_users = create_list(:user, 25)
これらのメソッドは特定の数のfactoryを構築または作成し, 配列として返します.
各factoryの属性を設定するには, 通常と同じようにハッシュを渡します.
twenty_year_olds = build_list(:user, 25, date_of_birth: 20.years.ago)
factoryごとに異なる属性を設定するために, これらのメソッドにはfactoryとインデックスをパラメータとしてブロックを渡すことができます:
twenty_somethings = build_list(:user, 10) do |user, i|
user.date_of_birth = (20 + i).years.ago
end
build_stubbed_listは, 完全に無効化されたインスタンスを提供します:
stubbed_users = build_stubbed_list(:user, 25) # array of stubbed users
一度に2つのレコードを作成するための*_pairメソッドのセットもあります:
built_users = build_pair(:user) # array of two built users
created_users = create_pair(:user) # array of two created users
複数の属性ハッシュが必要な場合は, attributes_for_listを生成します:
users_attrs = attributes_for_list(:user, 25) # array of attribute hashes
factoryで静的解析ツールを実行する
factory_botを使用すると、既知のfactoryを静的解析できます:
Lintingとは, コードを解析して潜在的なエラーを発見するプログラムを実行することです.
和訳にあたり, "静的解析"や"静的解析ツールを使用する"というように翻訳しています.
FactoryBot.lint
FactoryBot.lint は各factoryを作成し, 作成処理中に発生した例外を取得します. FactoryBot::InvalidFactoryError は, 作成できなかった (および対応する例外) factoryのリストとともに発生します.
FactoryBot.lint の推奨される使い方は, テストスイートが実行される前にタスクでこれを実行することです. before(:suite) で実行すると, 単一のテストを実行するときに, テストのパフォーマンスに悪影響を及ぼします.
テストスイートとは, ソフトウェアテストの目的や対象ごとに複数のテストケースをまとめたもの.
Rakeタスクの例:
# lib/tasks/factory_bot.rake
namespace :factory_bot do
desc "Verify that all FactoryBot factories are valid"
task lint: :environment do
if Rails.env.test?
conn = ActiveRecord::Base.connection
conn.transaction do
FactoryBot.lint
raise ActiveRecord::Rollback
end
else
system("bundle exec rake factory_bot:lint RAILS_ENV='test'")
fail if $?.exitstatus.nonzero?
end
end
end
FactoryBot.lint を呼び出した後, レコードが作成される可能性が高いので, データベースを削除することをお勧めします. 上記の例では, SQLトランザクションとロールバックを使用して, データベースを消去しています.
静的解析したいfactoryのみを渡すことで, 選択的にfactoryを静的解析することができます:
factories_to_lint = FactoryBot.factories.reject do |factory|
factory.name =~ /^old_/
end
FactoryBot.lint factories_to_lint
これはold_という接頭辞を持たないすべてのfactoryを静的解析することになります.
Traitsはlintedにすることもできます. このオプションは各factoryがもつすべてのtraitがそれ自体で有効なオブジェクトを生成することを検証します. これはlintメソッドにtraits: trueを渡すことで有効になります:
FactoryBot.lint traits: true
これは, 他の引数と組み合わせることもできます:
FactoryBot.lint factories_to_lint, traits: true
静的解析に使用するストラテジーを指定することもできます:
FactoryBot.lint strategy: :build
詳細な静的解析には, 各エラーの完全なバックトレースを含むので, デバッグに役立ちます:
FactoryBot.lint verbose: true
Custom Construction
もし factory_bot を使ってオブジェクトを構築し, initializeするためにいくつかの属性を渡す場合, あるいはビルドクラスで単にnewを呼ぶ以外のことをしたい場合は, factoryにinitialize_withを定義してデフォルトの動作をオーバーライドすることが可能です.
例:
# user.rb
class User
attr_accessor :name, :email
def initialize(name)
@name = name
end
end
# factories.rb
sequence(:email) { |n| "person#{n}@example.com" }
factory :user do
name { "Jane Doe" }
email
initialize_with { new(name) }
end
build(:user).name # Jane Doe
factory_botは, ActiveRecordで動作するように書かれていますが, 任意のRubyクラスで動作させることができます. ActiveRecordとの互換性を最大限に高めるために, デフォルトの初期化子は, 引数なしでビルドクラスでnewを呼び出すことにより, すべてのインスタンスをビルドします. そして, 属性記述メソッドを呼び出して, すべての属性値を代入します. これはActiveRecordでは正常に機能しますが, 他のほとんどのRubyクラスでは機能しません.
初期化子をオーバーライドすることができます:
-
initializeに引数を必要とするActiveRecord以外のオブジェクトの構築 - インスタンスの生成に
new以外のメソッドを使用する - ビルド後にインスタンスを装飾するようなワイルドなことができる
initialize_withを使用する場合, newを呼び出す際にクラス自体を宣言する必要はありません. しかし, 呼び出したい他のクラスメソッドは, 明示的にクラス上で呼び出す必要があります.
例:
factory :user do
name { "John Doe" }
initialize_with { User.build_with_name(name) }
end
attributesを呼び出すことにより, initialize_withブロック内のすべてのパブリック属性にアクセスすることもできます:
factory :user do
transient do
comments_count { 5 }
end
name "John Doe"
initialize_with { new(**attributes) }
end
これはnewに渡されるすべての属性のハッシュを構築します. これには一時的な属性は含まれませんが, factoryで定義された他のすべての属性が渡されます(関連付け, 評価済みシーケンスなど).
FactoryBot.defineブロックに含めることで, すべてのfactoryに対してinitialize_withを定義することができます:
FactoryBot.define do
initialize_with { new("Awesome first argument") }
end
initialize_withを使用する場合, initialize_withブロック内からアクセスされた属性はコンストラクタ内でのみ代入されます. これはおおよそ次のコードに相当します:
FactoryBot.define do
factory :user do
initialize_with { new(name) }
name { 'value' }
end
end
build(:user)
# runs
User.new('value')
4.0より前のバージョンのfactory_botでは, 次のように実行されていました:
FactoryBot.define do
factory :user do
initialize_with { new(name) }
name { 'value' }
end
end
build(:user)
# runs
user = User.new('value')
user.name = 'value'
カスタムストラテジー
カスタムのビルドストラテジーを追加することで, factory_botの動作を拡張したい場合があります.
ストラテジーは, associationとresultの2つの方法を定義します:
associationはFactoryBot::FactoryRunnerインスタンスを受け取ります. このインスタンスでrunを呼び出し, 必要に応じてストラテジーをオーバーライドできます. 2番目のメソッドresultは, FactoryBot::Evaluationインスタンスを受け取ります. これは, コールバック(notifyを使用), オブジェクトまたはハッシュ(factoryで定義された属性に基づいて結果インスタンスまたはハッシュを取得するため), およびfactoryで定義されたto_createコールバックを実行するcreateをトリガーする方法を提供します.
factory_botが内部でストラテジーをどのように使用するかを理解するには, 4つのデフォルトストラテジーのそれぞれのソースを表示するのがおそらく最も簡単です.
FactoryBot::Strategy::Createを使用して, モデルのJSON表現を構築するストラテジーを構成する例を以下に示します.
class JsonStrategy
def initialize
@strategy = FactoryBot.strategy_by_name(:create).new
end
delegate :association, to: :@strategy
def result(evaluation)
@strategy.result(evaluation).to_json
end
end
factory_botに新しいストラテジーを認識させるために, ストラテジーを登録します:
FactoryBot.register_strategy(:json, JsonStrategy)
これにより
FactoryBot.json(:user)
最後に, ストラテジーの代わりに新しいオブジェクトを登録することで, factory_bot自身のストラテジーをオーバーライドすることができます.
コールバックをカスタム
カスタムストラテジーを使用している場合, カスタムコールバックを定義することができます:
class JsonStrategy
def initialize
@strategy = FactoryBot.strategy_by_name(:create).new
end
delegate :association, to: :@strategy
def result(evaluation)
result = @strategy.result(evaluation)
evaluation.notify(:before_json, result)
result.to_json.tap do |json|
evaluation.notify(:after_json, json)
evaluation.notify(:make_json_awesome, json)
end
end
end
FactoryBot.register_strategy(:json, JsonStrategy)
FactoryBot.define do
factory :user do
before(:json) { |user| do_something_to(user) }
after(:json) { |user_json| do_something_to(user_json) }
callback(:make_json_awesome) { |user_json| do_something_to(user_json) }
end
end
オブジェクトを永続化するカスタムメソッド
デフォルトでは, インスタンス上でレコードを作成するとsave!が呼び出されます. これは常に理想的であるとは限らないため, factoryでto_createを定義することにより, その動作をオーバーライドできます:
factory :different_orm_model do
to_create { |instance| instance.persist! }
end
作成時に永続化メソッドを完全に無効にするには、そのfactoryのskip_createを使用します:
factory :user_without_database do
skip_create
end
すべてのfactoryでto_createをオーバーライドするには, FactoryBot.defineブロックの中で定義します:
FactoryBot.define do
to_create { |instance| instance.persist! }
factory :user do
name { "John Doe" }
end
end
Active SupportのInstrumentation機能
どのfactoryが作成されたか(そしてどのようなビルド戦略で)追跡するために, ActiveSupport::Notificationsが含まれており, 実行中のfactoryを購読する方法が提供されています. 一つの例として, 実行時間の閾値に基づいてfactoryを追跡することができます.
ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload|
execution_time_in_seconds = finish - start
if execution_time_in_seconds >= 0.5
$stderr.puts "Slow factory: #{payload[:name]} using strategy #{payload[:strategy]}"
end
end
他の例としては, すべてのfactoryと, それらがテストスイート全体でどのように使用されるかを追跡することができます. RSpecを使っている場合は, before(:suite)とafter(:suite)を追加するだけでよいでしょう:
factory_bot_results = {}
config.before(:suite) do
ActiveSupport::Notifications.subscribe("factory_bot.run_factory") do |name, start, finish, id, payload|
factory_name = payload[:name]
strategy_name = payload[:strategy]
factory_bot_results[factory_name] ||= {}
factory_bot_results[factory_name][strategy_name] ||= 0
factory_bot_results[factory_name][strategy_name] += 1
end
end
config.after(:suite) do
puts factory_bot_results
end
Rails PreloadersとRSpec
springやzeusなどのRails preloaderでRSpecを実行すると, 以下のように関連付けを持つfactoryを作成する際にActiveRecord::AssociationTypeMismatchというエラーが発生することがあります:
FactoryBot.define do
factory :united_states, class: "Location" do
name { 'United States' }
association :location_group, factory: :north_america
end
factory :north_america, class: "LocationGroup" do
name { 'North America' }
end
end
テストスイートの実行中にエラーが発生しました:
Failure/Error: united_states = create(:united_states)
ActiveRecord::AssociationTypeMismatch:
LocationGroup(#70251250797320) expected, got LocationGroup(#70251200725840)
2つの解決策は、プリローダー無しでスイートを実行するか, 次のようにRSpecの設定にFactoryBot.reloadを追加することです:
RSpec.configure do |config|
config.before(:suite) { FactoryBot.reload }
end
Bundlerなしでの使用
Bundlerを使用していない場合は, 必ずgemをインストールして呼び出してください:
require 'factory_bot'
spec/factoriesまたはtest/factoriesのディレクトリ構造になっていると仮定して, 必要なのは実行することだけです:
FactoryBot.find_definitions
factoryに別のディレクトリ構造を使用している場合, 定義を見つけようとする前に定義ファイルのパスを変更することができます:
FactoryBot.definition_file_paths = %w(custom_factories_directory)
FactoryBot.find_definitions
factoryの別ディレクトリがなく, インラインで定義したい場合はそれも可能です:
require 'factory_bot'
FactoryBot.define do
factory :user do
name { 'John Doe' }
date_of_birth { 21.years.ago }
end
end