FixtureからFactoryGirlへ

Fixture suck! と言われて久しいですね。こんにちは! onk です。

最近は Rails 3.0 でソーシャルアプリを作っています。で,BDD に RSpec 2.0 & FactoryGirl を使い出したので FactoryGirl についてご紹介。

define

まず,FactoryGirl は ActiveRecord に依存しています。factory の定義は AR のモデル単位。

Factory.define :onk, :class => User do |user|
  user.name  "onk"
  user.email "[email protected]"
end

たとえばこんな感じですね。

create / build

定義した factory を使うときは

Factory.create(:onk)
#=> #<User id: 1, name: "onk", email: "[email protected]", created_at: "2010-05-27 18:59:40", updated_at: "2010-05-27 18:59:40">

とか

Factory.build(:onk)
#=> #<User id: nil, name: "onk", email: "[email protected]", created_at: nil, updated_at: nil></code></pre>

とかになります。

create だとデータを保存してオブジェクトを返します。build は保存せずに返します。 User.new(params[:user]) みたいなものだと思えば OK。 あと Hash だけ欲しいときは

Factory.attributes_for(:onk)
#=> {:email=>"[email protected]", :name=>"onk"}</code></pre>

とします。

なお,factory の定義名=クラス名である場合は :class が省略できます。

Factory.define(:user) do |user|
  user.name "名無しさん"
end

使うときもデフォルトは create なので

Factory(:user)

で呼び出せます。

使い方まとめ

factory を定義する
`Factory.define`
保存されたオブジェクトを取得する
`Factory.create`
保存していないオブジェクトを取得する
`Factory.build`
Hash を取得する
`Factory.attributes_for`

他にも stub とかありますが,とりあえず以上だけ覚えておけば Fixture っぽく使えるかと思います。

ひとつだけ注意点。define した factory は全て Factory.factories に詰められてるだけなので,全 model で共通の名前空間になっています。 名前の付け方には注意してください。model 名で prefix, suffix を付けると分かりやすいですね。

relationship

+-------------------+
|       User        |
+-------------------+
| PK id      int    |
|    name    string |
+-------------------+
         | (user.id = post.user_id)
+-------------------+
|       Post        |
+-------------------+
| PK id      int    |
| FK user_id int    |
|    body    string |
+-------------------+

のような関連を表したいときは factory 定義の中で保存してしまえば良いです。

まずは has_one 関連の場合。

Factory.define :post do |p|
  p.body "オマエモナー"
end
Factory.define :user do |u|
  u.name "名無しさん"
  u.post Factory(:post)
end

で,:user を生成すると

u = Factory :user
#=> #<User id: 1, name: "名無しさん", email: "sage", created_at: "2010-05-27 19:19:37", updated_at: "2010-05-27 19:19:37">
u.post
#=> #<Post id: 1, user_id: 1, body: "オマエモナー", created_at: "2010-05-27 19:19:21", updated_at: "2010-05-27 19:19:37">

となり,見事に関連が張られています。

ちなみに :user と :post を書く順番を逆にすると

ArgumentError: No such factory: post

と怒られてしまいますので,読み込み順を深く考えたくない場合は {} で囲って遅延評価にしておきます。

Factory.define :user do |u|
  u.name "名無しさん"
  u.post {Factory(:post)}
end

has_many の場合は配列で定義します。

Factory.define :user do |u|
  u.name "名無しさん"
  u.posts {[Factory(:post), Factory(:post), Factory(:post)]}
end
u = Factory :user
u.posts.size #=> 3

関連を非常にすっきり書けますね。これが FactoryGirl の魅力の一つです。

callback

関連を記述しているとき,validate があると結構厄介です。先ほどの

+-------------------+
|       User        |
+-------------------+
| PK id      int    |
|    name    string |
+-------------------+
       | (user.id = post.user_id)
+-------------------+
|       Post        |
+-------------------+
| PK id      int    |
| FK user_id int    |
|    body    string |
+-------------------+

で,Post#user_id に

validates :user_id, :presence => true # 要は not_nil

をかけてるとしましょう。ありがちですね。

先ほどのままの factory 定義

Factory.define :user do |u|
  u.name "名無しさん"
  u.posts {[Factory(:post)]}
end
Factory.define :post do |p|
  p.body "オマエモナー"
end

では,:user を保存するより先に :post を保存することになります。このとき,まだ user_id が入っていないので validation に撥ねられます。

Factory :user
#=> ActiveRecord::RecordInvalid: Validation failed: User can't be blank

factory_girl/proxy/create.rb を読んでみると

class Factory
  class Proxy #:nodoc:
    class Create < Build #:nodoc:
      def result
        run_callbacks(:after_build)
        @instance.save!
        run_callbacks(:after_create)
        @instance
      end
    end
  end
end

となっています。つまり :after_build:after_create で処理を挟むことができます。

これを使えば,validation に引っかかるようなモデルも上手く書くことができますね。

Factory.define :user do |u|
  u.name "名無しさん"
  u.after_create do |user|
    user.posts = [Factory(:post, :user_id => user.id)]
  end
end
Factory.define :post do |p|
  p.body "オマエモナー"
end
u = Factory :user
#=> #<User id: 1, name: "名無しさん", email: nil, created_at: "2010-05-27 19:42:41", updated_at: "2010-05-27 19:42:41">
u.posts
#=> [#<Post id: 1, user_id: 1, body: "オマエモナー", created_at: "2010-05-27 19:42:41", updated_at: "2010-05-27 19:42:41">]

はい,綺麗に書けました。

あ,Factory(:post, :user_id => user.id) みたいに create 時に外から attribute を渡すこともできます。 なので「ちょこっとだけ違うオブジェクトを作りたい」とか言うときはテストの中でさらっと書いちゃえば良いと思います。

Factory(:user, :name => "名も無き冒険者")
#=> #<User id: 2, name: "名も無き冒険者", email: nil, created_at: "2010-05-27 19:44:29", updated_at: "2010-05-27 19:44:29">

sequence

unique 制約かけたいカラムってありますよね。そんなの相手に愚直に factory を数十個作るなんてもったいないです。sequence を使いましょう。

Factory.sequence(:google) do |n|
  "go" + "o" * n + "gle"
end
Factory.next(:google) #=> "google"
Factory.next(:google) #=> "gooogle"
Factory.next(:google) #=> "goooogle"
Factory.next(:google) #=> "gooooogle"

まぁ呼ぶたびにインクリメントするだけなので普通に n 返せば良いです(笑)

Factory.sequence :name do |n|
  "user_#{n}"
end
Factory.define :user, :class => :User do |u|
  u.name {Factory.next(:name)}
end

sequencenextを使うように定義しておけば

Factory.create(:user)
#=> <User id: 1, name: "user_1", email: nil, created_at: "2010-05-27 19:45:15", updated_at: "2010-05-27 19:45:15">
Factory.create(:user)
#=> <User id: 2, name: "user_2", email: nil, created_at: "2010-05-27 19:45:16", updated_at: "2010-05-27 19:45:16">

となります。

Factory.next{} と遅延評価にするのを忘れると,常に "user_1" が入っちゃうので気をつけて。

parent

factory の継承もサポートしています。

Factory.define :user do |u|
  u.name "名無しさん"
  u.email "sage"
end
Factory.define(:admin_user, :parent => :user) do |u|
  u.name "名無しさん@FOX★"
end
Factory :admin_user
#=> #<User id: 1, name: "名無しさん@FOX★", email: "sage", created_at: "2010-05-27 19:45:35", updated_at: "2010-05-27 19:45:35">

email が継承されていますね。 sequence と parent を上手く使いこなせば,Factory.define はそんなに書かなくても良いはずです。 Fixture を使っていたときにカオスになったのを思い出して,最低限の記述にすることを心がけましょう。

Fixture からの概念の変化

sequence や parent で見えてきましたね。 Fixture と FactoryGirl では概念がまったく違います。 Fixture にはオブジェクトの値を直接記述していました。 しかし,FactoryGirl で定義するものはオブジェクトではなく雛型です。 使うときに,雛型からオブジェクトを好きなだけ作れば良いのです。

冒頭で記述したような :onk というオブジェクトを定義するのは大きな誤り。 オブジェクトを定義してしまうと Fixture と変わらず,管理しづらいものができ上がると感じています。 雛型名は単なる :user ですね。他に何か定義するなら上記のような :admin_user や,post の有無で :posted_user を作る場合等がありそうです。

個人的にはなんとなく STI っぽいなと感じました。まぁ model のなかで class 作ってるようなモノなので。

faker との連携

雛型だと見切ったら,FactoryGirl と faker を同時に使うと非常に強力なことにも気づけるかと思います。

Factory.define :user do |u|
  u.name {Faker::Name.name}
  u.email {Faker::Internet.email}
end
Factory :user
#=> #<User id: 1, name: "Garland Keebler", email: "[email protected]", created_at: "2010-05-27 19:57:09", updated_at: "2010-05-27 19:57:09">
Factory :user
#=> #<User id: 2, name: "Marlee Mosciski Jr.", email: "[email protected]", created_at: "2010-05-27 19:57:10", updated_at: "2010-05-27 19:57:10">
Factory :user
#=> #<User id: 3, name: "Providenci Fisher", email: "[email protected]", created_at: "2010-05-27 19:57:11", updated_at: "2010-05-27 19:57:11">

Fixture からの解放は,単に関連記述を簡略化するだけではありません。 性質の違う雛型を性質名で定義し,使うときには雛型をもとに好きなようにオブジェクトを作る。 それが FactoryGirl の素晴らしい点だと僕は理解しています。

参考 URL