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 "onk@drecom.co.jp"
end
たとえばこんな感じですね。
create / build
定義した factory を使うときは
Factory.create(:onk)
#=> #<User id: 1, name: "onk", email: "onk@drecom.co.jp", 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: "onk@drecom.co.jp", created_at: nil, updated_at: nil></code></pre>
とかになります。
create
だとデータを保存してオブジェクトを返します。build
は保存せずに返します。
User.new(params[:user])
みたいなものだと思えば OK。
あと Hash だけ欲しいときは
Factory.attributes_for(:onk)
#=> {:email=>"onk@drecom.co.jp", :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
と sequence
と next
を使うように定義しておけば
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: "baron@wolff.ca", 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: "samanta@emard.uk", 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: "madie_boyer@kochgleichner.us", created_at: "2010-05-27 19:57:11", updated_at: "2010-05-27 19:57:11">
Fixture からの解放は,単に関連記述を簡略化するだけではありません。 性質の違う雛型を性質名で定義し,使うときには雛型をもとに好きなようにオブジェクトを作る。 それが FactoryGirl の素晴らしい点だと僕は理解しています。