カテゴリー:
Rails
タグ:
 Rails Rspec FactoryGirl Fixture valid_attributes

このエントリーをはてなブックマークに追加
更新日時:
2014年02月25日(火)
作成日時:
2013年08月06日(火)

前の記事 / 次の記事

Fixtureは充分にイケてると思っているので、基本Fixtureで満足なんだけど、
Fixtureにできないことをするために、FactoryGirlを使ってみようと思い立ったメモ。

自分が知らないだけでFixtureできるのかも知れないけど、
DBに保存はしないけど値だけ欲しい時がある。

Rspecで言うと "valid_attributes" である値。
今までは自力で適当にモジュールつくって読み込んでたんだけど、
折角FactoryGirlってgemがあるんだから使ってみようかと思った。

※ 適当にどんどん追加していったらかなり長くなってしまったので
暇が訪れたらまとめて新しい記事にしたいと思う(2014/02/25)
何年後になるかな・・w

手順

  1. factory_girl_railsのインストール
  2. spec_helper.rbへの定義追加
  3. モデルの生成
  4. FactoryGirlの定義
  5. FactoryGirlの呼び出し
  6. FactoryGirlの省略
  7. FactoryGirlの名前空間
  8. FactoryGirlのtrait
  9. FactoryGirlのnested_attributes
  10. FactoryGirlのファイルアップロード
  11. FactoryGirlのattributes_forのkeyを文字列にする
  12. FactoryGirlのsequence

1.factory_girl_railsのインストール

factory_girl_rails

$ vi Gemfile
gem 'factory_girl_rails', '~> 3.0', group: [:development, :test]

$ bundle install

2.spec_hepper.rb への定義追加

これはRspecで使う場合。
他のフレームワークで使う場合はそれっぽいところに定義する。

次の定義を追加することで、Spork や Spring を使っている時に、
factory girl での変更が再起動することなく即座に反映されるようになる。

但し、この定義を追加すると、 動作速度が遅くなる ので
テストを書いている時だけ有効にするのがいいと思う。

$ vi spec/spec_helper.rb
RSpec.configure do |config|
  config.before do
    FactoryGirl.reload
  end
end

3.モデルの生成

FactoryGirlをインストールしてからモデルを生成すると、
"spec/factories"以下にFactoryGirl用のファイルが一緒にできる。

$ rails g scaffold user name:string age:integer gender:string

とかするとできる。別にジェネレーターで生成しなければいけない訳ではない。

4.FactoryGirlの定義

基本

$ vi spec/factories/users.rb
FactoryGirl.define do
  factory :user do
    name   "Takashi"
    age    20  
    gender "male"
  end
end

モデル名と別の名前を付ける場合

$ vi spec/factories/users.rb
FactoryGirl.define do
  factory :female, class: User do
    name   "Asuka"
    age    20  
    gender "female"
  end
end

ここはファイル名が users.rb なんだからクラスは何も書かなくても User を参照して欲しいところだけど、
そうはならない。

5.FactoryGirlの呼び出し

buildしたオブジェクトを呼び出す(オブジェクトを生成するがDBには保存しないケース)

FactoryGirl.build(:user)
FactoryGirl.build(:female)

createしたオブジェクトを呼び出し(オブジェクトを生成してDBにも保存するケース)

FactoryGirl.create(:user)
FactoryGirl.create(:female)

attributesとして呼び出す(オブジェクトを生成せずにハッシュを生成する)

FactoryGirl.attributes_for(:user)
FactoryGirl.attributes_for(:female)

で、そもそもやりたかったことは、

describe UsersController do
  let(:valid_attributes) { FactoryGirl.attributes_for(:user) }
  describe "Post create" do
    describe "with valid params" do
      it "creates a new User" do
        expect {
          post :create, { user: valid_attributes }, valid_session
        }.to change(User, :count).by(1)
      end
    end
  end
end

6.FactoryGirlの省略

毎回毎回、

FactoryGirl.build(:user)
FactoryGirl.create(:user)
FactoryGirl.attributes_for(:user)

とか書くのはだるい。
でもコンフィグに一行、

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

と書けば(ファイル分けるのが面倒だったら"spec/spec_helper.rb"に直接書いても同じ)、

build(:user)
create(:user)
attributes_for(:user)

と書けるようになる。
けど自分の使い方だとcreateメソッドが競合する場合があったので、

$ vi spec/support/factory_girl.rb
FG = FactoryGirl

としてみた。

FG.build(:user)
FG.create(:user)
FG.attributes_for(:user)

と書ける。

7.FactoryGirlの名前空間

FactoryGirlは全てのファクトリー(っていうの?)で名前空間を共有する。

なんだって-!!

つまり

$ vi spec/factories/heros.rb
FactoryGirl.define do
  factory :edison, class: Hero do
    first_name "Thomas"
    last_name  "Edison"
  end
end

$ vi spec/factories/books.rb
FactoryGirl.define do
  factory :edison, class: Book do
    name   "Edison"
    isbn   なんか適当な値
  end
end

は衝突する。

なのでプレフィックスとかサフィックスを付けたりしてなんか頑張る。
(:hero_edison, :book_edisonとか。)

8.FactoryGirlのtrait

FactoryGirlは名前空間を共有しているので、
同じモデルで複数の定義をつくろうとすると名前が冗長になるけど、
traitの機能を使うと、同じ名前で複数のバリエーションを用意することができる。

FactoryGirl.define do
  factory :person do
    last_name  "もも"
    first_name "たろう"
    age 10

    trait :as_male do
      gender "male"
    end

    trait :as_female do
      gender "female"
    end

    trait :as_adult
      age 20
    end

    trait :as_female_name do
      last_name  "しらゆき"
      first_name "ひめ"
    end
  end
end

と書いて、次のように使うことができる。

基本

FactoryGirl.attributes_for(:person)
#=> { last_name: "もも", first_name: "たろう", age: 10 }

特定の属性を追加する

FactoryGirl.attributes_for(:person, :as_male)
#=> { last_name: "もも", first_name: "たろう", age: 10, gender: "male" }

FactoryGirl.attributes_for(:person, :as_female)
#=> { last_name: "もも", first_name: "たろう", age: 10, gender: "female" }

traitで定義したものの中に既に定義されているものがある場合、それは上書きされる

FactoryGirl.attributes_for(:person, :as_adult)
#=> { last_name: "もも", first_name: "たろう", age: 20 }

複数のtraitを組み合わせて使うことができる
(その場合、重複する値は後から指定された値に上書きされる。)

FactoryGirl.attributes_for(:person, :as_female_name, :as_male, :as_adult)
#=> { last_name: "しらゆき", first_name: "ひめ", age: 20, gender: "male" }

9.FactoryGirlのnested_attributes

参考:thoughtbot/factory_girl Support nested_attributes

class Book < ActiveRecord::Base
  belongs_to :author
  accept_nested_attributes_for :author
end

みたいなモデルがあった時

FactoryGirl.define do
  factory :book_with_author, class: Book do
    title "我が輩は猫である"
    author_attributes { attributes_for(:author) }
  end
end

FactoryGirl.define do
  factory :author do
    last_name  "夏目"
    first_name "漱石"
  end
end

みたいに書いて、

it "作者が同時に保存される" do
  expect {
    post :create FactoryGirl.attributes_for(:book_with_author)
  }.to change(Author, :count).by(1)
end

みたいに書ける。

10.FactoryGirlのファイルアップロード

ファイルアップロードする時どうすんの?

参考

anonymous / factories.rb
thoughtbot/factory_girl Trait not registered: class

include ActionDispatch::TestProcess
FactoryGirl.define do
  factory :photo do
    name  "富士山"
    image { fixture_file_upload("spec/files/MtFuji.jpg", "image/jpg") }
  end
end

ちなみにファイルアップロードするとテストが激遅になるのでピンポイントで使うのがいいと思う。

11.FactoryGirlのattributes_forのkeyを文字列にする

参考:stackoverflow Factory Girl with string attribute keys instead of symbols?

例えば

Article.should_receive(:new).with(FactoryGirl.attributes_for :article)
post :create, article: FG.attributes_for(:article)

とかすると、Railsで受け取ったパラメーターはキーにシンボルを使っている場合、
そのキーは文字列に変換されてparamsに格納されるので、
期待する値は受け取ってねーと言われてテストに失敗する。

例えば

{ "title" => "今日の出来事" } は receive してるけど { title: "今日の出来事"} は receive していないと言われる。

で、attributes_for で key を文字列にするには次のようにする。

FactoryGirl.attributes_for(:article).stringify_keys

12.FactoryGirlのsequence

今更sequenceに触れるのかよって感じだけど、
sequenceを使うと重複しないオブジェクトを自動的につくれる。

FactoryGirl.define do
  factory :book do
    sequence(:title) { |i| "こちら葛飾区亀有公園前派出所#{i}" }
    description "両さんがなんかします。"
  end
end

とかして

FactoryGirl.create :book

とすれば、実行される度に新しい連番が付いたこち亀が自動的に生成されていく。
次のように関連の中で使用した場合も同じ。

FactoryGirl.define do
  factory :review do
    book
    comment "100巻までは持ってる。"
  end
end

みたいにして

FactoryGirl.build :review

とすれば、実行される度に新しい連番が付いたこち亀が自動的に生成されていく。
(buildで書いているけどcreateでも同じ。)

気付いた注意点

従来のバージョンでは

FactoryGirl.create(:user)

ではなく、

Factory.create(:user)

のように書けたみたいだけど、これは非推奨になっていた。
今回使った factory_girl のバージョンは 3.6.2

参考