Make FactoryGirl Respect Uniqueness When Creating Associations

If you have an ActiveRecord model associated with another model that has a uniqueness constraint, modeling it in FactoryGirl when writing specs doesn’t work out of the box. But there is a simple solution.

Let’s say you have a UserType ActiveRecord model which has a value field, with a unique constraint. Which means that in your DB you don’t have two records in user_types table with the same value field. You would have entries with value being admin, guest, student, teacher, etc. Now in your specs you want to declare a factory for each UserType. So using FactoryGirl, you are going to write something like this:

FactoryGirl.define do
  factory :user_type do
    value "some_user_type"

    factory :admin_user_type do
      value "admin"
      adds_locations true
      adds_organizations true
      manage_teachers true
    end

    factory :guest_user_type do
      value "guest"
      adds_locations false
      adds_organizations false
      manage_teachers false
    end
   
   #... and so forth
  end
end

So far so good. You also decide to define factories for each kind of user:

FactoryGirl.define do
  factory :user do
    sequence(:email) { |n| "user-#{n}@myapp.com" }
    password "Password123"
    association :user_type, factory: :user_type

    factory :guest do
      association :user_type, factory: :guest_user_type
    end
  end
end

Now somewhere in your spec you create two different guest users:

guest1 = create(:guest)
guest2 = create(:guest)

Of course you expect each guest object to be associated with the same UserType object. Let’s check:

>> User.all.map{|u| [u.email, u.user_type_id]} 
# prints:[user-1@myapp.com, 1], [user-2@myapp.com, 2]

And here is the surprise: each guest is associated with a different UserType object, one with id=1 and another with id=2. This is certainly not what we expected, so what’s going on?

Turns out that every time you call create(:my_factory), FactoryGirl creates a new object for every association that my_factory has. And that’s a problem if you expect every guest to be associated with the same UserType object. So what do we do?

The solution is simple: FactoryGirl allows you to initialize your factories in way that would respect uniqueness. Let’s revisit our UserType factories:

FactoryGirl.define do
  factory :user_type do
    value "some_user_type"

    factory :admin_user_type do
      value "admin"
      adds_locations true
      adds_organizations true
      manage_teachers true
      initialize_with { UserType.find_or_create_by(value: 'admin') }
    end

    factory :guest_user_type do
      value "guest"
      adds_locations false
      adds_organizations false
      manage_teachers false
      initialize_with { UserType.find_or_create_by(value: 'guest') }
    end
  end
end

Using initialize_with we initialize each new guest user object with the same user_type we created previously. Let’s try to create two guests again and see if anything changed:

>> User.all.map{|u| [u.email, u.user_type_id]}
# prints: [user-1@myapp.com, 1], [user-2@myapp.com, 1]

Great, we see that now each user is indeed associated with the same guest user_type record which has id=1.

And now, feel free to go and make yourself a cup of coffee, you earned it.

Leave a Reply

Your email address will not be published. Required fields are marked *