いまさら聞けないfactory_girl入門

thoughtbot/factory_girlを活用して日々テストを書いているRailsエンジニアの方々、こんにちは、正徳です。

Forkwellでもfactory_girlを使ってテストを書いています。 このfactory_girlには便利機能が多く、とても使いやすいのですが、女心並みに複雑*1なので、一通りの機能をブログにまとめてみました。

factory_girl初心者から、中級者の参考になれば幸いです。

参考にしたページ

このブログの内容は全てfactory_girlのGETTING_STARTEDに記載されています。

読まれた事のない方は、是非一読する事をおすすめします。

目次

factory_girlのインストールと設定

factory_girlをRailsで使う場合のインストール方法と設定を紹介します。

インストール

いつものように、Gemfileにfactory_girl_railsを追加し、bundle installします。

group :development, :test do
  gem "factory_girl_rails", "~> 4.0"
end

本記事を書いた時のGETTING_STARTEDには"~> 4.0"と書かれていましたが、最新のバージョンが変わっている可能性があります。 インストールする前に最新のバージョンを確認した方が良いです。

設定

GETTING_STARTEDに従って設定します。

# spec/support/factory_girl.rb
RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end

spec/support/*のファイルはデフォルトで読み込まれないため、rails_helper.rbのコメントになっている箇所を修正します。

# spec/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

factory_girlの使い方(Using factories)

user has_many jobs through favoritesというシンプルなモデル構造を例にして、FactoryGirlの説明をしていきます。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :jobs, through: :favorites
end

# app/models/favorite.rb
class Favorite < ActiveRecord::Base
  belongs_to :user
  belongs_to :job
end

# app/models/job.rb
class Job < ActiveRecord::Base
end

build

factoryの対象はDBに保存せず、 関連するレコードをDBに保存する という振る舞いのストラテジです。 バリデーションのテストなど、関連レコードがDBに保存されている必要があるテストで使うと良いです。

favorite = build :favorite
#<Favorite id: nil, ...>
favorite.user
#<User id: 1, ...>

create

factoryの対象、関連レコードの 両方ともDBに保存する という振る舞いをするストラテジです。 whereなどDBからデータを取得するようなテストで使用するのが良いです。

favorite = create :favorite
#<Favorite id: 1, ...>
favorite.user
#<User id: 1, ...>

attributes_for

factoryの対象の属性を Hashで返す という振る舞いをするストラテジです。

user_attrs = attributes_for :user
{:user_name=>"tshotoku", :admin=>true, :first_name=>"Shotoku", ...}

これはコントローラのテストでpostする時に便利です。

context 'with valid attributes' do
  before { post :create, user: attributes_for(:user) }
  it { expect(response).to be_redirect }
end

context 'with invalid attributes' do
  before { post :create, user: attributes_for(:user, first_name: nil) }
  it { expect(response).to render_template(:new) }
end

build_stubbed

factoryの対象、関連レコードの両方とも DBに保存しない という振る舞いをするストラテジです。 DBのアクセスを必要とせず、インスタンスの属性だけでテストが可能な場合に使うと良いです。

ちなみに、idには適当な値が設定されます。

favorite = build_stubbed :favorite
#<Favorite id: 1003, ...>
favorite.user
#<User id: 1001, ...>

遅延評価(Lazy Attributes)

属性に現在時刻やメソッド呼び出しなどを使う場合、遅延評価する必要があります。

factory :job do
  # ...
  published_at { Time.now }
  pending_mail_token { Job.generate_token }
end

関連(Associations)

belongs_toの関連とfactoryが同名の場合、シンプルに関連を定義することができます。

factory :job do
  company
end

factoryと属性の名前が異なる場合、属性を上書きしたい場合などは下記のような指定も可能です。

factory :job do
  association :author, factory: :company, name: 'grooves'
end

ストラテジの変更

buildを使う際に関連レコードをDBに保存したくない場合、ストラテジを指定することができます。

association :author, factory: :company, strategy: :build

後述する特性(Traits)と組み合わせることで、通常のテストでは関連レコードを作り、特定のテストでは関連レコードを作成しない、といった制御も可能です。

factory :job do
  association :author, factory: :company

  trait :with_new_author do
    association :author, factory: :company, strategy: :build
  end
end

job = build :job
job.new_record? # => true
job.author.new_record? # => false

job_with_new_author = build :job, :with_new_author
job_with_new_author.new_record? # => true
job_with_new_author.author.new_record? # => true

連番(Sequences)

ユニークキー制約などで属性を変えたい場合、factory_girlによって連番を生成することができます。

factory :user do
  # person_1, person_2, person_3, ...
  sequence(:email) { |n| "person_#{n}@example.com" }
end

ちなみに、初期値を変えたり、数値以外を使うこと*2も可能です。

初期値を変える例

factory :user do
  # person_1000, person_1001, person_1002, ...
  sequence(:email, 1000) { |n| "person_#{n}@example.com" }
end

数値以外を使う例

factory :user do
  # person_a, person_b, person_c, ...
  sequence(:email, 'a') { |n| "person_#{n}@example.com" }
end

コールバック(Callbacks)

下記4つのコールバックがあり、各処理の前後で特定の処理を行うことができます。

  • before(:create) - create の実行前
  • after(:build) - build, create の実行後
  • after(:create) - create の実行後
  • after(:stub) - build_stubbed の実行後
factory :user do
  after(:build) { |user| generate_hashed_password(user) }
end

特性(Traits)

複数の属性をグループ化し、factoryに適用できます。

factory :job do
  title 'Title'

  trait :published do
    published_at { Time.now }
    title { "Job published at #{published_at}" }
  end

  trait :unpublished do
    published_at nil
  end

  trait :general_salary_range do
    salary_lower 400
    salary_upper 600
  end

  trait :rich_salary_range do
    salary_lower 700
    salary_upper 1_000
  end
end

Traitは組み合わせて使う事ができます。

job1 = create :job, :published, :general_salary_range
#<Job id: 1, title: "Job published at 2015-04-28 12:03:28", published_at: "2015-04-28 12:03:28", salary_lower: 400, salary_upper: 600>
job2 = create :job, :unpublished, :rich_salary_range
#<Job id: 2, title: "Title", published_at: nil, salary_lower: 700, salary_upper: 1000>

一時的な変数(Transient)

本来のクラスには存在しないが、Factoryでのみ使いたい一時的な変数を定義することができます。 これはコールバックと組み合わせる時に便利です。

factory :user do
  transient do
    friends_count 0
  end

  after(:create) do |user, evaluator|
    if evaluator.friends_count > 0
      create_list :friendship, evaluator.friends_count, user: user
    end
  end
end

複数レコードの生成(Building or Creating Multiple Records)

複数のレコードを生成したいときに使います。

built_users = build_list :user, 25
created_users = create_list :user, 25

また、createなどと同じく属性を上書きすることもできます。

# 10人の紳士が作られる
gentlemen = create_list :user, 10, age: 30, gender: :man

ActiveRecord以外での利用(Custom Construction)

一般的なRubyのクラス、ActiveModelのクラスでもinitialize_withを使うことでfactory_girlを利用することができます。

# app/models/user.rb
class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

# spec/factories/users.rb
factory :user do
  name "talkto_me"

  initialize_with { new name }
end

build(:user).name
#<User name: "talkto_me">

ActiveModelのように、コンストラクタにハッシュを渡せる場合はattributesを使うことも出来ます。

factory :user do
  name "talkto_me"
  age 17

  initialize_with { new attributes }
end

build(:user)
#<User name: "talkto_me", age: 17>

*1:女心並みって書いてあるけど、女心が分かる訳ではない

*2:#nextをサポートしているオブジェクトが指定可能です