Wednesday, April 3, 2013

FactoryGirl: how to fill tables with many-to-many relations

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 [] }
end
2. This field will be passed to evaluator.teachers to after action. Access it:
factory :subject_with_teachers do
  ignore { teachers [] }

  after(:create) do |subject, evaluator|
    evaluator.teachers.each do
      ...
    end
  end
end

Note: in after action we can write custom code, e.g. each cycle.

3. Pass this parameter from rspec test (or other test engine):
describe '...' do
  let(:teachers)       { FactoryGirl.create_list(:teacher, 3) }
  let(:subjects)       { FactoryGirl.create_list(:subject_with_teachers, 3, teachers: teachers) }
  ...
end

Full 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
end

Rspec 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