Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

FactoryBot GETTING_STARTED の和訳

Last updated at Posted at 2022-09-26

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_stubbedMarshal.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で構築することができます.

あなたのモデルがこのようなもので, 関連するStudentProfileが両方とも同じ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_listbuild_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_buildafter_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の動作を拡張したい場合があります.

ストラテジーは, associationresultの2つの方法を定義します:
associationFactoryBot::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

springzeusなどの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
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?