注: factory_girlはfactory_botに改名されました。
おなじみfactory_girlがちょっと分かってきたので覚え書きがてらメモです.
FactoryGirlって,あちこちで見る事例がシンプルすぎることが多くて手元のある程度複雑なデータ構造のFactoryをどうやって書くか,ピンと来ないことが多い気がします.要試行錯誤ですね.
前提Model
この記事で使うModelの前提です.掲示板(Forum)があり,そこに対して記事(Post)を投稿するのですが,記事には商品(Product)が紐付いている,というものです.
イメージとしては「おまえらのオススメガジェット教えろ」みたいなのがforumで,postには具体的なコメントと商品を選択して評価スコア(100点満点)と一緒に投稿できる,といったものになります.
Rails4前提なので,attr_accessibleは書いていません.
ソースコードは以下の通り.
class CreateSampleTables < ActiveRecord::Migration
def change
create_table :product do |t|
t.string :name
t.integer :price
end
create_table :forum do |t|
t.string :title
t.text :description
end
create_table :post do |t|
t.integer :post_id
t.integer :product_id
t.string :body
t.integer :score
end
end
end
各modelの定義は以下の通り
class Product < ActiveRecord::Base
has_many :posts
end
</pre>
<pre class="brush: ruby; toolbar: false;">
class Forum < ActiveRecord::Base
has_many :posts
end
</pre>
<pre class="brush: ruby; toolbar: false;">
class Post < ActiveRecord::Base
belongs_to :forum
belongs_to :product
end
Factoryの作成
さて,とりあえず何も考えずFactoryを書いてみます.良くある記述方法だと以下の様になるかと思います.
実際にはModelごとにfactoryファイルを分けますが,説明の都合上まとめます.
FactoryGirl.define do
factory :product do |f|
name 'MacBook Pro Retina 15 Early 2013'
price 218800
end
factory :forum do |f|
title 'おまいらのオススメガジェット教えれ'
end
factory :post do |f|
forum
product
body 'MBP 15 Retina,まじおすすめ.ドヤリングにぜひ!'
score 100
end
end
これで,とりあえずFactoryの生成テストを書いてみます.
これも普通はファイルを分けます
require 'spec_helper'
describe Product do
it 'has a valid factory' do
expect(FactoryGirl.create(:product)).to be_valid
end
end
describe Forum do
it 'has a valid factory' do
expect(FactoryGirl.create(:forum)).to be_valid
end
end
describe Post do
it 'has a valid factory' do
expect(FactoryGirl.create(:post)).to be_valid
end
end
ここまでが良くあるFactoryGirlのチュートリアルなどで書いてあるテストになります.
単一モデルのチェックであれば,これとMock/Stubを使えばテストを書いていくことができます.
Integration Testを書く
次に,複数Modelが関連するテストコードを書いてみます.ここでは,ForumとPost,Productが正しく相互に動いているかどうかを書いていきます.
ここでは,Forumの平均スコアを計算するaverage_score(product)のテストを書いてみます.
require 'spec_helper'
describe 'Forum Post integration' do
context 'ForumにPostが存在しないとき' do
before(:each) do
@forum = FactoryGirl.create(:forum)
end
it 'Forumの平均スコアは0' do
expect(@forum.average_score).to eq 0
end
end
context 'Forumに1件のPostがあるとき' do
before(:each) do
@post = FactoryGirl.create(:post)
@forum = @post.forum
@product = @post.product
end
it 'Forumの平均スコアは100(Postのfactoryスコアは100で,post 1件の場合)' do
expect(@forum.average_score).to eq 100
end
end
end
ここまではfactory定義されたデータだけなので,何の問題も無いですね.
では,postを増やして平均スコア計算が正しく動いているかをテストするコードを書いてみます.
# describeブロックやらspec_helperのrequireは省略
context 'Forumに2件以上のPostがあるとき' do
before(:each) do
# 一つ目のForum/Postまで作成
@post = FactoryGirl.create(:post)
@forum = @post.forum
@product = @post.product
end
it 'Forumに新たに50点のPostが入ったとき,平均スコアは75' do
FactoryGirl.create(:post, score: 50)
expect(@forum.average_score).to eq 75
end
end
これはうまくいきません.以前FactoryGirlでhas_manyな関係を定義する方法でも上がっている通り,これだとFactoryGirl.create(:post, score: 50)したときに,改めてForumとProductが生成されてしまうわけですね.
これに対応するには,一番単純な方法としてfactoryを分けるという方法があり,以下の様になります.
Factory
# 既存のpost定義
factory :post do |f|
forum
product
body 'MBP 15 Retina,まじおすすめ.ドヤリングにぜひ!'
score 100
end
# product, postは手動で設定してね
factory :post_template, class: Post do |f|
body 'MBP 15 Retina,まじおすすめ.ドヤリングにぜひ!'
score 100
end
spec
# describeブロックやらspec_helperのrequireは省略
context 'Forumに2件以上のPostがあるとき' do
before(:each) do
# 一つ目のForum/Postまで作成
@post = FactoryGirl.create(:post)
@forum = @post.forum
@product = @post.product
end
it 'Forumに新たに50点のPostが入ったとき,平均スコアは75' do
FactoryGirl.create(:post_template, score: 50, forum: @forum, product: @product)
expect(@forum.average_score).to eq 75
end
end
これで期待した通りに動きます.しかし,factoryにbody, scoreの重複記述があってちょっと嫌な感じです.また,post_templateの定義でわざわざクラス名を指定しないといけないのが煩雑ですね.この数ならまだしも,factoryの数が増えてくると面倒な感じです.
traitを使う
ここで,traitを使うと以下の様に書けます.
Factory
factory :post do |f|
body 'MBP 15 Retina,まじおすすめ.ドヤリングにぜひ!'
score 100
trait :with_dependents do |post|
forum
product
end
end
spec
# describeブロックやらspec_helperのrequireは省略
context 'Forumに2件以上のPostがあるとき' do
before(:each) do
# 一つ目のForum/Postまで作成
@post = FactoryGirl.create(:post, :with_dependents)
@forum = @post.forum
@product = @post.product
end
it 'Forumに新たに50点のPostが入ったとき,平均スコアは75' do
FactoryGirl.create(:post, score: 50, forum: @forum, product: @product)
expect(@forum.average_score).to eq 75
end
end
こんな感じで,factoryブロック内にtraitブロックを書くと,Factory呼び出しの時に引数オプションを付けるイメージで任意のコードを実行させることができます.
また,Factory呼び出しの時に指定するtraitは一つでなくてもよくて,
factory :post do |f|
body 'MBP 15 Retina,まじおすすめ.ドヤリングにぜひ!'
score 100
trait :with_forum do |post|
forum
end
trait :with_product do |post|
product
end
end
として,
FactoryGirl.create(:post, :with_forum, :with_product, body: 'BODY上書きしてみる')
みたいなこともできます.
最近のfactory_girlのGETTING_STARTEDは割と充実しているので,改めて読み直してみようと思いました.