読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Factory Girl Railsのチートシート

Rails中級 Rails Test

Railsのテストで複数のオブジェクトの作成を簡易に行えるFactoryGirl。
FactoryGirlについて基本的なことを知っていることを前提に、RailsでFactoryGirlを使うよく使う機能やTipsをまとめました。

動作確認

  • Rails 4.1.7
  • Factory Girl 4.4.0
  • Factory Girl Rails 4.4.1

目次


1. Factory Girlのインストール

Gemfilefactory_girl_railsを追加する。

# Gemfile
group :development, :test do
  gem 'factory_girl_rails'

  # その他必要に応じて
  gem 'rspec-rails'
  gem 'pry-rails'
  gem 'pry-byebug'
  ...
end


Bundlerを実行する

bundle install


2. FactoryGirlシンタックスの省略

include FactoryGirl::Syntax::Methodsを追加することで、次のようにFactoryGirlシンタックスを省略できるのでタイプ数が減り便利です。

# シンタックス省略前
FactoryGirl.create(:user) #=> Userモデルを作成

# シンタックス省略後
create(:user) #=> Userモデルを作成


FactoryGirlシンタックスを省略する設定

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

# rails_helper.rb
# コメントアウトを外す
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }



### MiniTestの場合 ###
class MiniTest::Unit::TestCase
  include FactoryGirl::Syntax::Methods
end


### MiniTest::Specの場合 ###
class MiniTest::Spec
  include FactoryGirl::Syntax::Methods
end


### minitest-railsの場合 ###
class MiniTest::Rails::ActiveSupport::TestCase
  include FactoryGirl::Syntax::Methods
end


3. FactoryGirlの使い方あれこれ

3.1. オブジェクトのビルド、作成、スタブ作成、属性取得

# 次のFactoryが定義されている前提
# spec/factories/users.rb
FactoryGirl.define do
  factory :user do
    name "MyString"
    sex "male"
    age 1
  end
end

# Userインスタンスを返す(DBに保存されていない)
user = build(:user)
#=> <User id: nil, name: "MyString", sex: "male", age: 1, created_at: nil, updated_at: nil>

# Userインスタンスを返す(DBに保存されている)
user = create(:user)
#=> <User id: 1, name: "MyString", sex: "male", age: 1, created_at: "2014-12-09 12:50:05", updated_at: "2014-12-09 12:50:05">

# すべての定義された属性が格納されたスタブ化されたオブジェクトを返す
stub = build_stubbed(:user)
#=> <User id: 1001, name: "MyString", sex: "male", age: 1, screated_at: nil, updated_at: nil>

# 通常のnew, create, updateに渡せる属性のハッシュを返す
attrs = attributes_for(:user)
#=> {:name=>"MyString", :sex=>"male", :age=>1}

3.2. 特定の値を指定してオブジェクトを作成

上記すべてのメソッドの第2引数にハッシュで属性名と値を渡すことで、特定の値を設定してオブジェクトを作成できる。

# name属性を"Tom", age属性を30で作成
user = build(:user, name: "Tom", age: 30)
#=> <User id: nil, name: "Tom", sex: "male", age: 30, created_at: nil, updated_at: nil>

3.3. ブロックを渡すことで細かな処理を記載可能

上記のすべてのメソッドでブロックが使える

# userオブジェクトとそれに紐づくpostオブジェクトを作成(+DB保存)している
create(:user) do |user|
  user.posts.create(attributes_for(:post))
end

3.4. 一度に複数のレコードを作成する

上記のすべてのメソッド_listを追加することで、一度に複数のオブジェクトの生成ができる。

# 25ユーザーを作成(DBに未保存)
build_users   = build_list(:user, 25)

# 25ユーザーを作成(DBに保存済)
created_users = create_list(:user, 25)

build_stubbed_list(:user, 25)
attributes_for_list(:user, 25)

# 特定の値を設定して、複数のオブジェクト作成することも可能
twenty_years_olds = create_list(:user, 25, age: 20)


4. Factory定義のあれこれ

4.1. 他の属性に依存する属性を定義する

# 定義
FactoryGirl.define do
  factory :user do
    # 名前にageの値も入れる
    name { "MyString-#{age}" }
    sex "male"
    age 1
  end
end

# 利用
create(:user)
# => <User id: 1, name: "MyString-1", age: 1, sex: "male", created_at: "...", updated_at: "..." >

4.2. factoryの階層構造で定義をDRYにしたい

Factoryを階層構造にすることでDRYに様々なFactoryを作成できます。

# 定義
FactoryGirl.define do
  factory :user do
    name { "MyString-#{age}" }
    sex "male"
    age 1

    # name属性だけ設定
    # それ以外は上位階層の値を設定
    factory :tom do
      name "Tom"
    end
  end
end

# 利用
build(:tom)
=> #<User id: nil, name: "Tom", sex: "male", age: 1, created_at: nil, updated_at: nil>

4.3. 特徴をわかりやすくする。グループ化させる

Trait(呼び方:トレイ、意味:特色/特性/特徴)を使うことで属性定義をわかりやすくできます。
また、Traitsを使うことで、Traitをグループ化することがdけいます。

# 定義
FactoryGirl.define do
  factory :product do
    name "My Awesome Product"

    trait :published do
      published true
      published_at Time.zone.now
    end

    trait :unpublished do
      published false
      published_at nil
    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

    # traitsでグループ化する
    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
end


### 使用 ###
# 第2引数にtraitのシンボルを渡すことで値を設定できる
build(:product, :published)
#=> <Product id: nil, name: "My Awesome Product", published: true, published_at: "2014-12-09 13:27:36", start_at: nil, end_at: nil, created_at: nil, updated_at: nil>

# factoryで定義したシンボル名でオブジェクトを作成できる
build(:week_long_published_product)
#=> <Product id: nil, name: "My Awesome Product", published: true, published_at: "2014-12-09 13:29:35", start_at: "2014-12-02 13:30:29", end_at: "2014-12-09 13:30:29", created_at: nil, updated_at: nil>

4.4. 呼び出しのたびに作成するデータを変える

sequenceメソッドを使うことで作成するデータを変えることができます。

# 定義
FactoryGirl.define do
  factory :post do
    sequence(:title)   { |n| "MyString#{n}" }
    sequence(:content) { |n| "MyText#{n}"   }
  end
end

# 使用
# 呼び出すたびにオートインクリメントされる(1, 2, 3, ...)
build(:post)
#=> <Post id: nil, user_id: nil, title: "MyString1", content: "MyText1", created_at: nil, updated_at: nil>
build(:post)
#=> <Post id: nil, user_id: nil, title: "MyString2", content: "MyText2", created_at: nil, updated_at: nil>

4.5. belongs_toの関連を定義

factory名(関連名)と同じ名前を指定することで関連先のモデルも一緒に作成できます。

# クラス定義
class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end


# ファクトリー定義
factory :user do
  # ...
end

factory :post do
  # ...

  # authorモデルも一緒に作成する
  author

  # 関連先のオブジェクトに値を設定できる
  # association :user, name: "Marin", sex: "female"
end


# 使用
# postオブジェクトとそれに関連するuserモデルも作成する
post = FactoryGirl.create(:post)
#=> <Post id: 2, user_id: 4, title: "MyString3", content: "MyText3", created_at: "2014-12-09 13:37:14", updated_at: "2014-12-09 13:37:14">
post.user
#=> <User id: 4, name: "MyString-1", age: 1, created_at: "2014-12-09 13:37:14", updated_at: "2014-12-09 13:37:14", sex: "male">

4.6. has_manyの関連を定義

# クラス定義
class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end


# ファクトリー定義
FactoryGirl.define do
  factory :user do
    name "MyString"

    factory :user_with_posts do
      # ignoreブロックでDBの属性とは関係ない属性を定義
      # FactoryGirl 4.5以降はignoreではなくtransientを使う
      ignore do
        posts_count 5
      end

      # userに関連したpostを作成する
      #   user - 作成されたuserインスタンス自身
      #   evaluator - ignore(transient)内の属性を含むファクトリのすべての属性を保持
      #   create_listの第2引数は、作成する関連をもったレコードの数を指定する
      after(:create) do |user, evaluator|
        create_list(:post, evaluator.posts_count, user: user)
      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


FactoryGirlでは`after(:create)`を含め以下4つのコールバックがあります。

after(:build)      # FactoryGirl.build, FactoryGirl.createでのbuild後に呼ばれる
before(:create) # FactoryGirl.createで保存される前に呼ばれる
after(:create)    # FactoryGirl.createで保存される後に呼ばれる
after(:stub)       # FactoryGirl.build_stubbedでスタブされた後に呼ばれる


5. Factoryのサンプル集

5.1. transientとinitialize_with

# Factory定義
factory :region do
  transient { continent nil }

  code 'M'
  name 'Comunidad de Madrid'

  country do
    if continent
      FactoryGirl.create(:country, continent: continent)
    else
      FactoryGirl.create(:country)
    end
  end

  # Regionをcodeとcountryで検索する、存在しない場合は作成する
  initialize_with do
    Region.find_or_initialize_by(code: code, country: country)
  end
end