What is FactoryGirl?
If you are new in Ruby testing, this part is for you.
FactoryGirl is nice replacement for fixtures. Some reasons to use it:
- FactoryGirl simplifies maintenance (when you add new field to database, you have to add it only to one place - appropriate factory).
- FactoryGirl allows to generate different values to the same object.
- You can think about objects and relations between them, not about IDs.
Official documentation (with good examples) here.
Fill tables with many-to-many relations
Tables structure
Documentations doesn't say it, but FactoryGirl allows to write custom code in before and after blocks. It's very convenient for filling many-to-many relations.
Imagine, there are 2 tables - teachers and subjects and relation many-to-many between them. Migration for creating these tables:
create_table :teachers do |t|
  t.string :name
end
create_table :subjects do |t|
  t.string :name
end
create_table :appointments do |t|
  t.references :teacher
  t.references :subject
end
Main idea
1. Declare in factory ignored field teachers, which not exists in class Subject, but can be passed from test:
factory :subject_with_teachers do
  ignore { teachers [] }
endafter action. Access it:
factory :subject_with_teachers do
  ignore { teachers [] }
  after(:create) do |subject, evaluator|
    evaluator.teachers.each do
      ...
    end
  end
endNote: in after action we can write custom code, e.g. each cycle.
describe '...' do
  let(:teachers)       { FactoryGirl.create_list(:teacher, 3) }
  let(:subjects)       { FactoryGirl.create_list(:subject_with_teachers, 3, teachers: teachers) }
  ...
endFull code
Factories
FactoryGirl.define do
  factory :teacher do
    sequence(:name) {|n| "name #{n}"}
  end
  factory :appointment, class: Appointment do
    teacher # should be passed by creator
    subject # should be passed by creator
  end
  factory :subject do
    sequence(:name) {|n| "name #{n}"}
    # inherits all subject fields
    factory :subject_with_teachers do
      # creates `field` 'teachers' with default value '[]'
      # caller can set this parameter when calls FactoryGirl.build or FactoryGirl.create
      ignore { teachers [] }
      after(:create) do |subject, evaluator|
        # subject contains only fields from factory
        # evaluator contains all fields, including ignored fields
        # here we can write any code, for example - each cycle
        puts "#{subject} has been created"
        evaluator.teachers.each do |teacher|
          FactoryGirl.create(:appointment, { teacher: teacher, subject: subject })
          puts "Appointment for #{teacher} and #{subject} has been created"
        end
      end
    end
  end
endRspec test
describe 'subjects with teachers' do
  let(:teachers)       { FactoryGirl.create_list(:teacher, 3) }
  let(:subjects)       { FactoryGirl.create_list(:subject_with_teachers, 3, teachers: teachers) }
  it 'should create 3 subjects, 3 teachers and 9 appointments between them' do
    subjects.length.should == 3
    subjects[0].teachers.should == teachers
    subjects[1].teachers.should == teachers
    subjects[2].teachers.should == teachers
  end
end