@kyanny's blog

My life. Opinions are my own.

factory_girl またはその他の fixture replacement で親子関係のあるモデルクラスを定義するのがなぜ難しいかという話

長いな。うまくかみ砕けてない証拠ですね。

GitHub みたいなものを作ってるんだと思ってください: こういうモデルがあるとする。有料課金していないユーザーは非公開レポジトリを1個までしか作れない。

class User < ActiveRecord::Base
  has_many :repositories

  def paid?
    self.payment_flag
  end
end

class Repository < ActiveRecord::Base
  belongs_to :user

  def before_create
    unless user.paid?
      if self.publicity == false && user.repositories.count(:conditions => ['publicity = ?', false]) > 0
        return false
      end
    end
  end
end

これに対するテストを書く: すいませんソラで書いてて実際動かしてないので間違ってるかもしれないです。

class RepositoryTest < Test::Unit::TestCase
  def setup
    Factory.define(:user) do |user|
    end
    Factory.define(:paid_user), :class => User do |user|
      user.payment_flag :true
    end
    Factory.define(:repository) do |repository|
      repository.user { Factory.create(:user) }
      repository.publicity true
    end
    Factory.define(:private_repository), :class => Repository do |repository|
      repository.user { Factory.create(:user) }
      repository.publicity false
    end
  end

  def test_create_private_repository
    private_repository = Factory.create(:private_repository)
    assert_kind_of Repository, private_repository
    other_private_repository = Factory.create(:private_repository)
    assert_nil other_private_repository # fail
  end
end

最後のアサーションは、「無料プランのユーザーは非公開レポジトリを複数持てない」ということをテストしたくて書いたのだけど失敗する。なぜなら private_repository と other_private_repository はそれぞれ別の user インスタンスを参照しているから。 Factory.define のなかで遅延評価で親となるリレーションを記述してしまうと、共通の親を持たせることができない。で、結局こんな風にしてしまう:

  def test_create_private_repository
    user = Factory.create(:user)
    private_repository = Factory.create(:private_repository, :user => user) # inject user instance manually...
    assert_kind_of Repository, private_repository
    other_private_repository = Factory.create(:private_repository, :user => user) # ...and inject same user instance manually, again. NOT DRY :(
    assert_nil other_private_repository
  end

Factory.create(:private_repository, :user => user) ってところ、 :user => user を毎回書くのはだるいしかっこ悪いですよね、ていうかこうやって毎度毎度自分でオブジェクトの親子関係を考慮して初期データを注入してあげなきゃいけないんじゃ fixture replacement を使うメリットがあんまりなくね?それならふつうの fixture を頑張って真面目に書いたほうが Factory.create を何度も書く必要もなくなってより DRY なんじゃね?

...というのが嫌だよね、という話だったと理解しています。結局ここは machinist (は最近開発が滞ってるらしいけど) でも fabrication でも変わらず、やっぱり難しいというか、難解ではないし解決不能でもないけどスマートなやり方はなくて悩ましいね、というのが今のところの結論だったように思う。