FactoryGirlでhas_manyな関係を定義する方法

FactoryGirl でhas_manyな関連を定義する方法が、ドキュメントをみただけでは分かりにくかったのでメモを残します

gemのVersionはそれぞれ

  • Ruby: 1.9.3-p392
  • Rails: 3.2.13
  • FactoryGirl: 4.2.0

サンプルとして科目(Subject)が複数の単元(Unit)を持っているというデータを作成します

Model

# app/models/subject.rb
  class Subject < ActiveRecord::Base
    has_many :units

    validates_uniquness_of :code

    attr_accessible :code, :name    
  end

# app/models/unit.rb
  class Unit < ActiveRecord::Base
    belongs_to :subject

    validates_uniquness_of :code

    attr_accessible :code, :name    
  end

Subject、Unitともに codeとnameを持っていて、codeの重複は禁止

ファクトリーの定義を作る

ファクトリーの定義は spec/factories.rb というファイルに記述します

  • spec/factories.rb
# coding: utf-8
FactoryGirl.define do
  factory :english, class: Subject do
    code "subject_01"
    name "英語"
  end

  factory :grammar, class: Unit do
    association :subject, factory: :english
    code "unit_01"
    name "文法"
  end

  factory :idiom, class: Unit do
    association :subject, factory: :english
    code "unit_02"
    name "熟語"
  end
end
  • spec/model/sample_spec.rb
describe 'sample' do
  it 'Subject:英語 が作成できる' do
    english = FactoryGirl.create(:english) # 無事作成されました
    english.should_not be_nil              # 結果はgreenです
  end
  it 'Unit:文法、熟語 が作成できる' do
    FactoryGirl.create(:grammar)           # 無事作成されました
    FactoryGirl.create(:idiom)             # uniquness制約によるエラーが出ました
    Unit.all.should have(2).items          # 「熟語」にあたるUnitが作成されなかったので結果はredになります
  end
end

うまくいきませんね
Subjectのcodeが重複してしまいます

今の定義だとUnitを作成する時点で毎回Subjectを作成しようとするようです
Subjectを複数回作成してしまうと、code(subject_01)が重複するのでエラーになりますね

解決方法

重複を回避するには、Sequencesという機能が使えます
これは自動的に連番を生成してくれる機能なので
code1 code2 code3… と重複しないcodeを設定できます

Subjectの定義を変更する

  factory :english, class: Subject do
    code sequence(:code) {|n| "subject_#{n}" }
    name "英語"
  end

今度は成功しました

しかし、今の状態ではUnitがそれぞれ別のSubjectを持ってしまっています
SubjectとUnitが1対1(has_one) という関係に近い状態ですね
一つのSubjectが複数のUnitを持っているという状態(has_many)を実現できていません

テストコード側の対策

同じSubjectを参照するようにするためには、テストコード側で対策が必要です
FactoryGirlは定義されたattributesをoverride する機能があるので、
Unit作成時にSubjectのインスタンスを渡すことで同一のSubjectをセットできます

      it 'Unit:文法、熟語 が作成できる' do
        @english = FactoryGirl.create(:english)
        FactoryGirl.create(:grammar, subject: @english)
        FactoryGirl.create(:idiom, subject: @english)
        Unit.all.should have(2).items
      end

これで、文法と熟語が同じSubjectに関連する状態になりました

ファクトリーの定義だけでこれと同じことが出来ると良いのですが、やり方が見当たらなかったのでこうなっています
ファクトリーの定義だけで完結する書き方を知っていたら教えて下さい

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ