Get two associations within a Factory to share another association
Asked Answered
R

6

20

I've got these 5 models: Guardian, Student, Relationship, RelationshipType and School. Between them, I've got these associations

class Guardian < ActiveRecord::Base
  belongs_to :school
  has_many :relationships, :dependent => :destroy
  has_many :students, :through => :relationships
end

class Student < ActiveRecord::Base
  belongs_to :school
  has_many :relationships, :dependent => :destroy
  has_many :guardians, :through => :relationships
end

class Relationship < ActiveRecord::Base
  belongs_to :student
  belongs_to :guardian
  belongs_to :relationship_type
end

class School < ActiveRecord::Base
  has_many :guardians, :dependent => :destroy
  has_many :students, :dependent => :destroy
end

class RelationshipType < ActiveRecord::Base
  has_many :relationships
end

I want to write a FactoryGirl which defines a relationship. Every relationship must have a guardian and a student. These two must belong to the same school. The guardian factory has an association with school, and so does the student factory. I've been unable to get them to be built in the same school. I've got the following code:

FactoryGirl.define do

  factory :relationship do
    association :guardian
    association :student, :school => self.guardian.school
    relationship_type RelationshipType.first
  end

end

This results in the following error when I try to build a relationship using this factory:

undefined method `school' for #<FactoryGirl::Declaration::Implicit:0x0000010098af98> (NoMethodError)

Is there any way to do what I want, to make the guardian and the student belong to the same school without having to resort to passing already created guardians and students to the factory (which is not its purpose)?

Roberson answered 11/1, 2012 at 13:31 Comment(1)
I'm not sure if this has anything to do with the error, but the School class was written as a second Relationship class declaration (before my edit).Wallis
R
10

I think this should work:

FactoryGirl.define do
  factory :relationship do 
    association :guardian
    relationship_type RelationshipType.first
    after_build do |relationship|
      relationship.student = Factory(:student, :school => relationship.guardian.school)
    end
  end
end
Return answered 14/1, 2012 at 18:43 Comment(0)
C
12

This answer is the first result on Google for 'factory girl shared association' and the answer from santuxus really helped me out :)

Here's an update with the syntax from the latest version of Factory Girl in case anyone else stumbles across it:

FactoryGirl.define do
  factory :relationship do
    guardian
    relationship_type RelationshipType.first

    after(:build) do |relationship|
      relationship.student = FactoryGirl.create(:student, school: relationship.guardian.school) unless relationship.student.present?
    end
  end
end

The unless clause prevents student from being replaced if it's been passed into the factory with FactoryGirl.create(:relationship, student: foo).

Carrol answered 26/1, 2013 at 19:51 Comment(0)
R
10

I think this should work:

FactoryGirl.define do
  factory :relationship do 
    association :guardian
    relationship_type RelationshipType.first
    after_build do |relationship|
      relationship.student = Factory(:student, :school => relationship.guardian.school)
    end
  end
end
Return answered 14/1, 2012 at 18:43 Comment(0)
N
4

There is cleaner way to write this association. Answer got from this github issue.

FactoryGirl.define do
  factory :relationship do 
    association :guardian
    student { build(:student, school: relationship.guardian.school) }
    relationship_type RelationshipType.first
  end
end
Nacred answered 5/10, 2015 at 16:47 Comment(1)
Based on the issue it looks like that line should be student { build(:student, school: guardian.school) } instead (relationship is omitted)Sacrosanct
T
3

Extending on nitsas' solution, you can abuse @overrides to check whether the guardian or student association have been overridden, and use the school association from the guardian/student. This allows you to override not only the school, but also just the guardian or just the student.

Unfortunately, this relies on instance variables, not public API. Future updates could very well break your factories.

factory :relationship do
  guardian { create(:guardian, school: school) }
  student  { create(:student,  school: school) }

  transient do
    school do
      if @overrides.key?(:guardian)
        guardian.school
      elsif @overrides.key?(:student)
        student.school
      else
        create(:school)
      end
    end
  end
end
Threnode answered 18/1, 2018 at 9:38 Comment(1)
this seems to be working really well for me, the most flexible, and especially good in my case where I've added a second association to a model that needs to match up with an existing association. This made almost all my existing tests "just work" and kept their expressivity, eg "If this set of tests really cares about the guardian, then yeah go ahead and make a new generic school" or vice versa. Of course, as noted, it's not a good idea (meh, it works!) - but is there any documentation on this or other FactoryBot internals, or did you just read the code?Dagon
W
1

This isn't really an answer you are seeking, but it seems that the difficulty in creating this association is suggesting that the table design may need to be adjusted.

Asking the question What if the user changes school?, the school on both the Student and Guardian needs to be updated, otherwise, the models get out of sync.

I put forward that a student, a guardian, and a school, all have a relationship together. If a student changes school, a new Relationship is created for the new school. As a nice side effect this enables a history to exist of where the student has been schooled.

The belongs_to associations would be removed from Student and Guardian, and moved to Relationship instead.

The factory can then be changed to look like this:

factory :relationship do
  school
  student
  guardian
  relationship_type
end

This can then be used in the following ways:

# use the default relationship which creates the default associations
relationship = Factory.create :relationship
school = relationship.school
student = relationship.student
guardian = relationship.guardian

# create a relationship with a guardian that has two charges at the same school
school = Factory.create :school, name: 'Custom school'
guardian = Factory.create :guardian
relation1 = Factory.create :relationship, school: school, guardian: guardian
relation2 = Factory.create :relationship, school: school, guardian: guardian
student1 = relation1.student
student2 = relation2.student
Walterwalters answered 7/2, 2016 at 23:41 Comment(0)
E
0

I'd use transient & dependent attributes in this case:

FactoryGirl.define do
  factory :relationship do
    transient do
      school { create(:school) }
      # now you can even override the school if you want!
    end

    guardian { create(:guardian, school: school) }
    student { create(:student, school: school) }
    relationship_type RelationshipType.first
  end
end

Usage:

relationship = FactoryGirl.create(:relationship)

relationship.guardian.school == relationship.student.school
# => true

And you can even override the school if you want:

awesome_school = FactoryGirl.create(:school)
awesome_relationship = FactoryGirl.create(:relationship, school: awesome_school)

awesome_relationship.guardian.school == awesome_school
# => true
awesome_relationship.student.school == awesome_school
# => true
Endless answered 27/1, 2017 at 9:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.