@kyanny's blog

My thoughts, my life. Views/opinions are my own.

FactoryGirl で has_many/belongs_to な association を書くときハマった

Rails のテストを書いていて、フィクスチャはのちのちメンテナンスに苦労しそうだから FactoryGirl を使ってみた。

FactoryGirl についてはここが詳しい http://www.func09.com/wordpress/archives/532

上のページの例にあるとおり、中間テーブルを使うようなケースでテストデータを定義するのに便利らしい (公式ページ?でもそのように宣伝していた) のだが、中間テーブルが不要な has_many/belongs_to の関係を定義するときに、 association を親子どちらに書くかでハマってしまった。

すぐ動かせるコード例がなくて恐縮だけど、 FactoryGirl で has_many/belongs_to な association を定義するときは、必ず子のほうに親の Factory を書かないといけないっぽい。このスレッド参照。 Google グループ

はしょったサンプルコードを書くと、こんな感じ:

### models ###
# app/models/user.rb
class User < ActiveRecord::Base
  has_many :bookmarks
end

# app/models/bookmark.rb
class Bookmark < ActiveRecord::Base
  belongs_to :user
end

### wrong case ###
# test/factories/user.rb
Factory.define(:sasimi), :class => User do |u|
  u.name "Kensuke Kaneko"
  u.bookmarks {
    [Factory.create(:30daysalbum), Factory.create(:ficia)] # ココがダメ
  }
end

# test/factories/bookmark.rb
Factory.define (:30daysalbum), :class => Bookmark do |b|
  b.url 'http://30d.jp/',
end
Factory.define (:ficia), :class => Bookmark do |b|
  b.url 'http://ficia.com/',
end

### test code ###
class UserTest < Test::Unit::TestCase
  def test_sasimi_has_2_bookmarks
    sasimi = Factory.create(:sasimi) # Factory.create する途中でエラー
    assert_equal 2, sasimi.bookmarks.size
  end
end

### right case ###
# test/factories/user.rb
Factory.define(:sasimi), :class => User do |u|
  u.name "Kensuke Kaneko"
end

# test/factories/bookmark.rb
Factory.define (:30daysalbum), :class => Bookmark do |b|
  b.url 'http://30d.jp/',
  b.user { Factory.create(:sasimi) } # こっちに書かないといけない
end
Factory.define (:ficia), :class => Bookmark do |b|
  b.url 'http://ficia.com/',
  b.user { Factory.create(:sasimi) } # ...でもコレは別の理由でエラーになることも
end

### test code ###
class UserTest < Test::Unit::TestCase
  def test_sasimi_has_2_bookmarks
    30daysalbum = Factory.create(:30daysalbum)
    ficia = Factory.create(:ficia)
    # なんか自分で書いてて間違ってる気がしてきましたが...子のほうから Factory.create してやらないといけない
    sasimi = Factory.create(:sasimi)
    assert_equal 2, sasimi.bookmarks.size
  end
end

File: README — Documentation for thoughtbot/factory_girl (master) を読んでみても、中間テーブルを利用するケースは例があるのだけど、ふつうの has_many/belongs_to の場合については記述がなかった(とおもう)。これは不親切だと思う。

あと、上のコード例のなかにコメントで書いたけど、 User モデルに validates_uniqueness_of :name があると、 30daysalbum と ficia を Factory.create するときに同じ user インスタンスを二度 create してしまうので、バリデーションエラーになる。これはまぁ当然の話だし回避策もいちおうあって、 sequence というのを使うとユニークな値を持った Factory を簡単に定義できる。

Factory.sequence(:yappo) do |n|
  "yap" + "p" * n + "o"
end
Factory.next(:yappo) #=> yappo
Factory.next(:yappo) #=> yapppo

なんだけど、これだと user のインスタンスがそれぞれ別のものになってしまう。同じユーザーが複数のブックマークを持ってる、という状態をつくりたいときは(has_manyなんだからそうしたいはず) bookmark のインスタンスを作るときに user のインスタンスを渡してやらないといけない。

user = Factory.create(:sasimi)
30daysalbum = Factory.create(:30daysalbum, :user => sasimi)
ficia = Factory.create(:ficia, :user => sasimi)

これはちょっと面倒くさい。


そんなわけで、 FactoryGirl を使ってみた。感想としては、フィクスチャよりは把握しやすいかな?という気がした。べつに YAML で書いておくメリットも特に思いつかないし、だったら Ruby で書いてあってもいいよねと。ただまぁやりすぎると結局ふつうにテストコードの中で User.create とかしてるのとあんま違いがないのでは、という風になっていってしまうのかな。あと上に書いたように少し面倒なところがある。まぁこの例だったらフィクスチャでよくね?って話なのかもしれない。 FactoryGirl よりも Machinist のほうがよさげという話もどこかで見かけたので、そちらも試して比較してみたいなと思う。