FactoryBotの定義チートシート

factory_bot 6.5.0、factory_bot_rails 6.4.4で確認。

利用方法はFactoryBotの利用方法チートシートを参照。

定義ファイル

RSpecを使っている場合、デフォルトでは spec/factories/[modelファイル名の複数形].rb になる。例えば spec/factories/users.rb など。

しかし、ファイルが別れていたとしても名前空間が別れたりするわけでもなく、factoryの定義はグローバルに定義される。例えば、以下のような設定をした場合は FactoryBot::DuplicateDefinitionError でエラーになる。

# spec/factories/users.rb

  factory :admin, class: 'User' do
    account_name { 'admin_account' }
  end

# spec/factories/admins.rb

  factory :admin do
    permission_type { :full }
  end

そのため、ファイル名に何らかの意味があるかといえば、挙動の面において何の影響力もなく任意のファイル名にすることができる。さらにspec/factories/foo/bar.rbのようにapp/models/配下のディレクトリ構成とは別の構成でディレクトリを作ることもできる。

ただ、可能であるという話であり、わざわざ標準から外れる選択肢をとるかは別問題である。

基本定義

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'suzuki' }
  end
end

FactoryBot.defineのブロックは定型文。factoryメソッドの引数のシンボル(以下、Factory名とする)は、buildメソッドなどに渡す第一引数に対応する。Factory名がModel名のスネークケースに一致する場合は、そのModelのインスタンスが作られる。

上記例のnameはusersテーブルのカラム名で、ブロックには初期値を指定する。つまり、上記定義がある場合はFactoryBotを使う場合と直接Modelをインスタンス化する場合は以下のように対応する。

build(:user)
User.new(name: 'suzuki')

create(:user)
User.create(name: 'suzuki')

古い文献では name { 'suzuki' } の代わりに name 'suzuki' とブロックなし定義がされていることがあるが、これはfactory_bot v5.0.0で削除された記法であり、現代ではブロックを指定する記法しかサポートされていない。rubocop-factory_botを入れているのであれば、FactoryBot/AttributeDefinedStaticallyによって古い記法での記述は警告される。

インスタンスを作るクラスの指定

前述の通り、Factory名がModel名のスネークケースならインスタンス化されるのはそのModelになる。Factory名がModel名に対応しない場合はクラス名を明示的に指定する必要がある。

# spec/factories/users.rb
FactoryBot.define do
  factory :admin, class: 'User' do
    name { 'suzuki' }
  end
end

classキーワード引数に指定する値は、文字列でなくとも class: Userのようにクラスの定数を指定することも可能ではある。しかしrubocop-factory_botを入れているのであれば、FactoryBot/FactoryClassNameで警告されるため文字列を指定しておくべきである。

blongs_toアソシエーション、has_oneアソシエーション

相互に接続したアソシエーションの問題は別記。

FactoryBotの定義においては、blongs_toもhas_oneも区別がない。

例えばBookモデルが以下のようになっていたとする。

class Book < ApplicationRecord
  belongs_to :author
end

この場合、spec/factories/books.rbは以下のようにすることで、authorアソシエーションを定義できる。

FactoryBot.define do
  factory :book do
    # spec/factories/authors.rb に `factory :author`の定義がある前提
    author
  end
end

なお、association :authorのように書くことも可能だが、調べた限りでは違いはない。また、rubocop-factory_botを入れているのであれば、FactoryBot/AssociationStyleで警告されるので、factoryメソッドブロックの直下ではassociationは使わない方が良いだろう。オプションで変えられるので好み次第ではある。

has_manyアソシエーション

相互に接続したアソシエーションの問題は別記。

例えばAuthorモデルが以下のようになっていたとする。

class Author < ApplicationRecord
  has_many :books
end

この場合、spec/factories/authors.rbは以下のようにすることで、booksアソシエーションを定義できる。

FactoryBot.define do
  factory :author do
    # spec/factories/books.rb に `factory :book`の定義がある前提
    books { Array.new(5) { association(:book) }.compact }
  end
end

一見するとbooks { build_list(:book, 5) }でも良さそうに思えるが、build_stubbedattributes_forメソッドで思った挙動にならないためやめた方が良い。build_stubbedメソッドではidなどに値が入らないし、attributes_forメソッドではbooksキーにBookモデルのインスタンスの配列が入ってしまう。

compactを付けている理由は、attributes_forメソッドを呼んだ時に { books: [nil, nil, nil, nil, nil] }のようになってしまうため。compactなしだとAuthor.new(attributes_for(:author))ActiveRecord::AssociationTypeMismatchでエラーになってしまうが、compactをつけることでエラーを回避できる。

作成するbooksアソシエーションの数を制御したい場合は、transientを用いる。

FactoryBot.define do
  factory :author do
    transient do
      books_count { 5 }
    end
    # spec/factories/books.rb に `factory :book`の定義がある前提
    books { Array.new(books_count) { association(:book) }.compact }
  end
end

このような定義をすることでbuild(:author, books_count: 10)のようにインスタンス生成時に生成個数を制御できるようになる。

after(:create)などのコールバックを使って定義する方法もあるようだが、おそらくは上記が正攻法である。少なくとも、buildcreatebuild_stubbedattributes_forのいずれもうまくいくシンプルな記述方法になる。

after(:create)などを使った方法については公式のhas_many associationsのセクションを確認してほしい。

相互に接続したアソシエーション

以下のような相互に接続したアソシエーションがある場合、そのままFactoryBotの定義に記述してしまうと無限ループに陥ってしまう。

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end
FactoryBot.define do
  factory :author do
    books { Array.new(books_count) { association(:book) }.compact }
  end
end

FactoryBot.define do
  factory :book do
    author
  end
end

build(:author)を呼んだ場合、factory :author内のbooksアソシエーションからbuild(:book)が呼ばれ、factory :bookの定義が読まれauthorアソシエーションからbuild(:author)が呼ばれ…という形になり無限ループとなる。

この無限ループを断ち切る方法は、associationメソッドの引数でアソシエーションのオーバーライドをしてしまえば良い。

FactoryBot.define do
  factory :author do
    books { Array.new(books_count) { association(:book, author: instance) }.compact }
  end
end

FactoryBot.define do
  factory :book do
  author { association(:author, books: [instance]) }
  end
end

instanceは各々のアソシエーションを持つインスタンスになる。そのため、以下の通り整合性の取れたデータとなる。

author = create(:author)
author.books.first.author == author # => true

アソシエーション名と異なるFactory名を指定する

何らかの理由でアソシエーション名とFactory名が異なる場合、factoryキーワード引数を指定することで目的を達成できる。

FactoryBot.define do
  factory :book do
    author factory: :user
  end
end

Factory名のエイリアス

機能として存在するので記載するが、「アソシエーション名と異なるFactory名を指定する」方法をとった方がわかりやすいように思う。

factoryメソッドの引数にaliases: [...]を指定することでFactory名にエイリアスをつけることができる。

# spec/factories/users.rb

FactoryBot.define do
  factory :user, aliases: [:author] do
    name { 'suzuki' }
  end
end

これは、Model名とアソシエーション名が異なる場合に有用になる。例えば、Bookモデルのアソシエーション定義が以下のようになっていたとする。

class Book < ApplicationRecord
  belongs_to :author, class_name: 'User'
end

このBookモデルに対して以下のようにuserを用いてアソシエーションを定義することはできない。

# spec/factories/books.rb
# これはうまくいかない
FactoryBot.define do
  factory :book do
    user
  end
end

# こっちはうまくいく
FactoryBot.define do
  factory :book do
    author
  end
end

他属性の参照

他の属性の値を参照することもできる。

FactoryBot.define do
  factory :user do
    name { 'suzuki' }
    email { "#{name}@example.com" }
  end
end

このようにするとbuild(:user)とした時のemailの値はsuzuki@example.comとなる。また、build(:user, name: 'saito')とした時はemailの値はsaito@example.comとなる。

属性だけでなくアソシエーションも参照することができる。例えば、ブログホスティングサービスで、以下のようなモデルが定義されているとする。

class User < ApplicationRecord
  has_many :blogs
  has_many :entries
end

class Blog < ApplicationRecord
  belongs_to :user
  has_many :entries
end

class Entry < ApplicationRecord
  belongs_to :blog
  # blog経由で到達できるがショートカットで外部キーがあるとする
  belongs_to :user
end

このような場合、BlogのFactoryではblog.userblog.entries.first.userが一致するようにしなければならないが、アソシエーションも参照可能なので以下のように対処ができる。

FactoryBot.define do
  factory :blog do
    user
    entries { Array.new(5) { association(:entry, blog: instance, user: user) }.compact }
  end
end

FactoryBot.define do
  factory :entry do
    blog { association(:blog, entries: [instance], user: user) }
    user
  end
end

ポイントは、associationメソッドの引数user: userである。名前が同じだからわかりづらいが、association(:blog, entries: [instance], user: user)の場合、Blogから参照するUserのインスタンスにEntryですでにインスタンス化しているUserのインスタンスを指定している。

シーケンス

自動連番を生成する仕組みもある。

FactoryBot.define do
  # factoryメソッドのブロック外である点に注意
  sequence :account_name do |n|
    "account#{n}"
  end
end

これを利用するには、generateメソッドを利用する。

FactoryBot.define do
  factory :user do
    account_name { generate(:account_name) }
  end
end

上記の例のようにSequence名と属性名が一致している場合は単に以下のようにできる。

FactoryBot.define do
  factory :user do
    account_name # シーケンスで生成された値になる
  end
end

ここまでの例はグローバルなシーケンスの例だが、属性単位でシーケンスを定義することもできる。

FactoryBot.define do
  factory :user do
    sequence(:account_name) { |n| "account#{n}" }
  end
end

sequenceメソッドの第二引数にシーケンスの初期値を指定することもできる。

FactoryBot.define do
  factory :user do
    sequence(:account_name, 50) { |n| "account#{n}" }
  end
end

この初期値は、nextメソッドに応答するイテレータであれば何でも良い。

FactoryBot.define do
  factory :user do
    # :a, :b ... :z, :aa, :ab ...
    sequence(:account_name, :a) { |n| "account#{n}" }
    # 'a', 'b' ... 'z', 'aa', 'ab' ...
    sequence(:account_name, 'a') { |n| "account#{n}" }
    # 同じ値が生成され得るがこういうことも可能
    sequence(:account_name, ['suzuki', 'tanaka', 'saito'].cycle) { |n| "account#{n}" }
  end
end

また、ブロックを省略した場合はブロック引数の値がそのまま設定される。

sequenceメソッドにaliasesキーワード引数を指定することでSequence名のエイリアスを作れるようであるが、試した限りではおそらくグローバルなシーケンスでしか利用できない。

ファクトリのネストと継承

factoryメソッドはネストすることができる。

FactoryBot.define do
  factory :user do
    name { 'suzuki' }
    email { "#{name}@example.com" }

    factory :admin do
      name { 'admin' }
    end
  end
end

build(:admin)を実行した際はUser.new(name: 'admin', email: 'admin@example.com')が実行されたのと同じ結果となる。

つまり、親の属性などは引き継ぐし、子で定義した属性があればそちらが優先される。

また、ネストせずに特定のファクトリから引き継ぐ記法もある。

FactoryBot.define do
  factory :user do
    name { 'suzuki' }
    email { "#{name}@example.com" }
  end

  # ネストしていないが、parentを指定していることでuserファクトリから属性を引き継いでいる
  factory :admin, parent: :user do
    name { 'admin' }
  end
end

トレイト

factoryメソッドをネストすれば属性の継承ができるため、共通部分を親で定義して子でバリエーションを持たせるということはできる。

しかし、例えばProductモデルのファクトリを定義する際に、セール品、定価品と物理商品と電子商品という切り口でファクトリを用意するとなると面倒なことになる。少なくとも片方の切り口はダブらせて書かざるをえない。

FactoryBot.define do
  factory :product do
    factory :sale_product do
      # セール品の属性が並ぶ

      factory :sale_physical_product do
        # 物理商品の属性が並ぶ
      end

      factory :sale_electronic_product do
        # ...
      end
    end

    factory :regular_price_product do
      # 定価品の属性が並ぶ

      factory :regular_price_physical_product do
        # sale_physical_productのところと全く一緒のものが並ぶ
      end

      factory :regular_price_electronic_product do
        # sale_electronic_productのところと全く一緒のものが並ぶ
      end
    end
  end
end

こういう場合はtraitメソッドを利用すると便利である。

FactoryBot.define do
  factory :product do
    trait :sale_product do
      # セール品の属性
    end

    trait :regular_price_product do
      # 定価品の属性
    end

    trait :physical_product do
      # 物理商品の属性
    end

    trait :electronic_product do
      # 電子商品の属性
    end

    factory :sale_physical_product, traits: [:sale_product, :physical_product]
    factory :sale_electronic_product, traits: [:sale_product, :electronic_product]
    factory :regular_price_physical_product, traits: [:regular_price_product, :physical_product]

    # 以下のようにトレイトを属性のように書くことも可能
    factory :regular_price_electronic_product do
       regular_price_product
       electronic_product
    end
  end
end

複数指定しているトレイトで同じ属性の定義をしている場合は、配列の最後側のトレイトの定義が優先される。

以下のように、ファクトリ定義とトレイトの属性が衝突した場合は常にファクトリ定義の物が優先される。

trait :admin_name do
  name { '管理者' }
end

# build(:admin).name # => 'admin'
factory :admin do
  # トレイトと属性の順序に関係なく属性定義の内容が優先される
  name { 'admin' }
  admin_name
end

トレイトはfactoryだけではなく、associationメソッドでも利用できるし、buildメソッドなどインスタンスを作るメソッドでも利用できる。

コールバック

コールバックとして、以下の4つがある。

  • after(:build): buildとcreateで実行
  • before(:create): createで実行
  • after(:create): createで実行
  • after(:stub): build_stubbedで実行

after(:build, :create)のように複数のコールバック対象を指定することが可能。

lint

FactoryBotの定義が妥当かチェックする仕組みもある。