Using factory_girl in Rails with associations that have unique constraints. Getting duplicate errors
Asked Answered
M

10

42

I'm working with a Rails 2.2 project working to update it. I'm replacing existing fixtures with factories (using factory_girl) and have had some issues. The problem is with models that represent tables with lookup data. When I create a Cart with two products that have the same product type, each created product is re-creating the same product type. This errors from a unique validation on the ProductType model.

Problem Demonstration

This is from a unit test where I create a Cart and put it together in pieces. I had to do this to get around the problem. This still demonstrates the problem though. I'll explain.

cart = Factory(:cart)
cart.cart_items = [Factory(:cart_item, 
                           :cart => cart, 
                           :product => Factory(:added_users_product)),
                   Factory(:cart_item, 
                           :cart => cart, 
                           :product => Factory(:added_profiles_product))]

The two products being added are of the same type and when each product is created it is re-creating the product type and creating duplicates.

The error that gets generated is: "ActiveRecord::RecordInvalid: Validation failed: Name has already been taken, Code has already been taken"

Workaround

The workaround for this example is to override the product type being used and pass in a specific instance so only one instance is used. The "add_product_type" is fetched early and passed in for each cart item.

cart = Factory(:cart)
prod_type = Factory(:add_product_type)   #New
cart.cart_items = [Factory(:cart_item,
                           :cart => cart,
                           :product => Factory(:added_users_product,
                                               :product_type => prod_type)), #New
                   Factory(:cart_item,
                           :cart => cart,
                           :product => Factory(:added_profiles_product,
                                               :product_type => prod_type))] #New

Question

What is the best way to use factory_girl with "pick-list" types of associations?

I'd like for the factory definition to contain everything instead of having to assemble it in the test, although I can live with it.

Background and Extra Details

factories/product.rb

# Declare ProductTypes

Factory.define :product_type do |t|
  t.name "None"
  t.code "none"
end

Factory.define :sub_product_type, :parent => :product_type do |t|
  t.name "Subscription"
  t.code "sub"
end

Factory.define :add_product_type, :parent => :product_type do |t|
  t.name "Additions"
  t.code "add"
end

# Declare Products

Factory.define :product do |p|
  p.association :product_type, :factory => :add_product_type
  #...
end

Factory.define :added_profiles_product, :parent => :product do |p|
  p.association :product_type, :factory => :add_product_type
  #...
end

Factory.define :added_users_product, :parent => :product do |p|
  p.association :product_type, :factory => :add_product_type
  #...
end

The purpose of ProductType's "code" is so the application can give special meaning to them. The ProductType model looks something like this:

class ProductType < ActiveRecord::Base
  has_many :products

  validates_presence_of :name, :code
  validates_uniqueness_of :name, :code
  #...
end

factories/cart.rb

# Define Cart Items

Factory.define :cart_item do |i|
  i.association :cart
  i.association :product, :factory => :test_product
  i.quantity 1
end

Factory.define :cart_item_sub, :parent => :cart_item do |i|
  i.association :product, :factory => :year_sub_product
end

Factory.define :cart_item_add_profiles, :parent => :cart_item do |i|
  i.association :product, :factory => :add_profiles_product
end

# Define Carts

# Define a basic cart class. No cart_items as it creates dups with lookup types.
Factory.define :cart do |c|
  c.association :account, :factory => :trial_account
end

Factory.define :cart_with_two_different_items, :parent => :cart do |o|
  o.after_build do |cart|
    cart.cart_items = [Factory(:cart_item, 
                               :cart => cart, 
                               :product => Factory(:year_sub_product)),
                       Factory(:cart_item, 
                               :cart => cart, 
                               :product => Factory(:added_profiles_product))]
  end
end

When I try to define the cart with two items of the same product type, I get the same error described above.

Factory.define :cart_with_two_add_items, :parent => :cart do |o|
  o.after_build do |cart|
    cart.cart_items = [Factory(:cart_item,
                               :cart => cart,
                               :product => Factory(:added_users_product)),
                       Factory(:cart_item,
                               :cart => cart,
                               :product => Factory(:added_profiles_product))]
  end
end
Merrel answered 6/1, 2010 at 19:10 Comment(0)
F
32

I encountered the same problem and added a lambda at the top of my factories file that implements a singleton pattern, which also regenerates the model if the db has been cleared since the last round of tests/specs:

saved_single_instances = {}
#Find or create the model instance
single_instances = lambda do |factory_key|
  begin
    saved_single_instances[factory_key].reload
  rescue NoMethodError, ActiveRecord::RecordNotFound  
    #was never created (is nil) or was cleared from db
    saved_single_instances[factory_key] = Factory.create(factory_key)  #recreate
  end

  return saved_single_instances[factory_key]
end

Then, using your example factories, you can use a factory_girl lazy attribute to run the lambda

Factory.define :product do |p|
  p.product_type  { single_instances[:add_product_type] }
  #...this block edited as per comment below
end

Voila!

Fayth answered 25/8, 2010 at 18:27 Comment(6)
Excellent solution! Thanks for sharing. It makes usage simple and elegant. Just what I needed. :)Merrel
Thanks! This is what I was looking for too. Note that you can omit the "return" line in your lambda, since either the begin or the rescue block will already return the right thing.Excise
There is a small typo in the second code block. It should say 'single_instances' instead of 'single_instance'. Great solution, nonetheless.Branen
Is there a way to use the single_instance lambda outside of a factory block? I have considered the work-around of Factory(:product).product_type but would like to have a more direct way to the product_type.Masque
Very nice solution. Seems like this should be built into Factory Girl.Polycarp
This worked great. Agreed, no return saved_single_instances[factory_key] required.Eccrine
R
49

Just FYI, you can also use the initialize_with macro inside your factory and check to see if the object already exists, then don't create it over again. The solution using a lambda (its awesome, but!) is replicating logic already present in find_or_create_by. This also works for associations where the :league is being created through an associated factory.

FactoryGirl.define do
  factory :league, :aliases => [:euro_cup] do
    id 1
    name "European Championship"
    rank 30
    initialize_with { League.find_or_create_by_id(id)}
  end
end
Reconvert answered 10/7, 2012 at 7:23 Comment(5)
Of course, this relies on ActiveRecord, but for most people that shouldn't be a problem. Simple, elegant, and flexible...very nice.Sympathy
Plus, this will work with nested factories and other attributes as well. Great solution!Skyla
unfortunately find_or_create_by_id becomes deprecated in rails 4. and find_or_create_by fails for some reason..Sirocco
thanks for the heads up Pavel. This answer is quite old and probably needs to be revisited!Reconvert
@PavelK. Instead of initialize with use League.where( id: 1, name: "European Championship", rank: 30).first_or_createGarda
F
32

I encountered the same problem and added a lambda at the top of my factories file that implements a singleton pattern, which also regenerates the model if the db has been cleared since the last round of tests/specs:

saved_single_instances = {}
#Find or create the model instance
single_instances = lambda do |factory_key|
  begin
    saved_single_instances[factory_key].reload
  rescue NoMethodError, ActiveRecord::RecordNotFound  
    #was never created (is nil) or was cleared from db
    saved_single_instances[factory_key] = Factory.create(factory_key)  #recreate
  end

  return saved_single_instances[factory_key]
end

Then, using your example factories, you can use a factory_girl lazy attribute to run the lambda

Factory.define :product do |p|
  p.product_type  { single_instances[:add_product_type] }
  #...this block edited as per comment below
end

Voila!

Fayth answered 25/8, 2010 at 18:27 Comment(6)
Excellent solution! Thanks for sharing. It makes usage simple and elegant. Just what I needed. :)Merrel
Thanks! This is what I was looking for too. Note that you can omit the "return" line in your lambda, since either the begin or the rescue block will already return the right thing.Excise
There is a small typo in the second code block. It should say 'single_instances' instead of 'single_instance'. Great solution, nonetheless.Branen
Is there a way to use the single_instance lambda outside of a factory block? I have considered the work-around of Factory(:product).product_type but would like to have a more direct way to the product_type.Masque
Very nice solution. Seems like this should be built into Factory Girl.Polycarp
This worked great. Agreed, no return saved_single_instances[factory_key] required.Eccrine
N
3

EDIT:
See an even cleaner solution at the bottom of this answer.

ORIGINAL ANSWER:
This is my solution to creating FactoryGirl singleton associations:

FactoryGirl.define do
  factory :platform do
    name 'Foo'
  end

  factory :platform_version do
    name 'Bar'
    platform {
      if Platform.find(:first).blank?
        FactoryGirl.create(:platform)
      else
        Platform.find(:first)
      end
    }
  end
end

You call it e.g. like:

And the following platform versions exists:
  | Name     |
  | Master   |
  | Slave    |
  | Replica  |

In this way all 3 platform versions will have same platform 'Foo', i.e. singleton.

If you wanna save a db query you can instead do:

platform {
  search = Platform.find(:first)
  if search.blank?
    FactoryGirl.create(:platform)
  else
    search
  end
}

And you can consider to make the singleton association a trait:

factory :platform_version do
  name 'Bar'
  platform

  trait :singleton do
    platform {
      search = Platform.find(:first)
      if search.blank?
        FactoryGirl.create(:platform)
      else
        search
      end
    }
  end

  factory :singleton_platform_version, :traits => [:singleton]
end

If you want to setup more than 1 platform, and have different sets of platform_versions, you can make different traits which are more specific, i.e.:

factory :platform_version do
  name 'Bar'
  platform

  trait :singleton do
    platform {
      search = Platform.find(:first)
      if search.blank?
        FactoryGirl.create(:platform)
      else
        search
      end
    }
  end

  trait :newfoo do
    platform {
      search = Platform.find_by_name('NewFoo')
      if search.blank?
        FactoryGirl.create(:platform, :name => 'NewFoo')
      else
        search
      end
    }
  end

  factory :singleton_platform_version, :traits => [:singleton]
  factory :newfoo_platform_version, :traits => [:newfoo]
end

Hope this is useful to some out there.

EDIT:
After submitting my original solution above, I gave the code another look, and found an even cleaner way to do this: You do not define traits in the factories, instead you specify the association when you call the test step.

Make regular factories:

FactoryGirl.define do
  factory :platform do
    name 'Foo'
  end

  factory :platform_version do
    name 'Bar'
    platform
  end
end

Now you call the test step with the association specified:

And the following platform versions exists:
  | Name     | Platform     |
  | Master   | Name: NewFoo |
  | Slave    | Name: NewFoo |
  | Replica  | Name: NewFoo |

When doing it like this, the creation of platform 'NewFoo' is using 'find_or_create_by' functionality, so the first call creates the platform, the next 2 calls finds the already created platform.

In this way all 3 platform versions will have same platform 'NewFoo', and you can create as many sets of platform versions as you need.

I think this is a very clean solution, since you keep the factory clean, and you even make it visible to the reader of your test steps that those 3 platform versions all have the same platform.

Newkirk answered 1/12, 2011 at 14:44 Comment(2)
Your cleaner solution at the bottom looks nice but doesn't allow other attributes of NewFoo to be controlled. For example, what if the associated model was a person with fields 'firstname' and 'lastname'?Standin
True, it only allows for one attribute to be controlled. It has, however, solved most of my usecases. Maybe it is possible to rewrite the teststep to be able to control n number of attributes.Newkirk
M
2

The short answer is, "no", Factory girl doesn't have a cleaner way to do it. I seemed to verify this on the Factory girl forums.

However, I found another answer for myself. It involves another sort of workaround but makes everything much cleaner.

The idea is to change the models that represent the lookup tables to create the required entry if missing. This is OK because the code is expecting specific entries to exist. Here is an example of the modified model.

class ProductType < ActiveRecord::Base
  has_many :products

  validates_presence_of :name, :code
  validates_uniqueness_of :name, :code

  # Constants defined for the class.
  CODE_FOR_SUBSCRIPTION = "sub"
  CODE_FOR_ADDITION = "add"

  # Get the ID for of the entry that represents a trial account status.
  def self.id_for_subscription
    type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_SUBSCRIPTION])
    # if the type wasn't found, create it.
    if type.nil?
      type = ProductType.create!(:name => 'Subscription', :code => CODE_FOR_SUBSCRIPTION)
    end
    # Return the loaded or created ID
    type.id
  end

  # Get the ID for of the entry that represents a trial account status.
  def self.id_for_addition
    type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_ADDITION])
    # if the type wasn't found, create it.
    if type.nil?
      type = ProductType.create!(:name => 'Additions', :code => CODE_FOR_ADDITION)
    end
    # Return the loaded or created ID
    type.id
  end
end

The static class method of "id_for_addition" will load the model and ID if found, if not found it will create it.

The downside is the "id_for_addition" method may not be clear as to what it does by its name. That may need to change. The only other code impact for normal usage is an additional test to see if the model was found or not.

This means the Factory code for creating the product can be changed like this...

Factory.define :added_users_product, :parent => :product do |p|
  #p.association :product_type, :factory => :add_product_type
  p.product_type_id { ProductType.id_for_addition }
end

This means the modified Factory code can look like this...

Factory.define :cart_with_two_add_items, :parent => :cart do |o|
  o.after_build do |cart|
    cart.cart_items = [Factory(:cart_item_add_users, :cart => cart),
                       Factory(:cart_item_add_profiles, :cart => cart)]
  end
end

This is exactly what I wanted. I can now cleanly express my factory and test code.

Another benefit of this approach is the lookup table data doesn't need to be seeded or populated in migrations. It will handle itself for test databases as well as production.

Merrel answered 13/1, 2010 at 17:37 Comment(2)
Try posting this in github issues of factory_girl github.com/thoughtbot/factory_girl/issuesTrypsin
Try searching github.com/thoughtbot/factory_girl/issues for 'singleton' before you suggest that :(Standin
F
2

These problems would be eliminated when the singletons are introduced into factories- its currently at -http://github.com/roderickvd/factory_girl/tree/singletons Issue - http://github.com/thoughtbot/factory_girl/issues#issue/16

Fatherland answered 19/1, 2010 at 19:34 Comment(2)
Excellent! Thanks for telling me about this. I'll be watching this.Merrel
See also github.com/thoughtbot/factory_girl/issues/24 and github.com/thoughtbot/factory_girl/issues/148. The factory_girl team are apparently burying their heads in the sand on this.Standin
D
2

I had a similar situation. I ended up using my seeds.rb for defining the singletons and then requiring the seeds.rb in the spec_helper.rb to create the objects into the test database. Then I can just search the appropriate object in the factories.

db/seeds.rb

RegionType.find_or_create_by_region_type('community')
RegionType.find_or_create_by_region_type('province')

spec/spec_helper.rb

require "#{Rails.root}/db/seeds.rb"

spec/factory.rb

FactoryGirl.define do
  factory :region_community, class: Region do
    sequence(:name) { |n| "Community#{n}" }
    region_type { RegionType.find_by_region_type("community") }
  end
end
Drislane answered 4/3, 2012 at 19:26 Comment(0)
C
1

I've had this same problem, and I think it's the same one referenced here: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+validates_uniqueness_of#ef22581f4cd05aa9

I think your workaround is possibly the best solution to the problem.

Concenter answered 7/1, 2010 at 4:57 Comment(0)
M
1

Inspired by the answers here I found the suggestion from @Jonas Bang the closest to my needs. Here's what worked for me in mid-2016 (FactoryGirl v4.7.0, Rails 5rc1):

FactoryGirl.define do
  factory :platform do
    name 'Foo'
  end

  factory :platform_version do
    name 'Bar'
    platform { Platform.first || create(:platform) }
  end
end

Example of using it to create four platform_version's with the same platform reference:

FactoryGirl.create :platform_version
FactoryGirl.create :platform_version, name: 'Car'
FactoryGirl.create :platform_version, name: 'Dar'

=>

-------------------
 platform_versions
-------------------
 name | platform
------+------------
 Bar  | Foo
 Car  | Foo
 Dar  | Foo

And if you needed 'Dar' on a distinct platform:

FactoryGirl.create :platform_version
FactoryGirl.create :platform_version, name: 'Car'
FactoryGirl.create :platform_version, name: 'Dar', platform: create(:platform, name: 'Goo')

=>

-------------------
 platform_versions
-------------------
 name | platform
------+------------
 Bar  | Foo
 Car  | Foo
 Dar  | Goo

Feels like the best of both worlds without bending factory_girl too far out of shape.

Mcnamara answered 13/6, 2016 at 6:0 Comment(0)
M
0

I think I at least found a cleaner way.

I like the idea of contacting ThoughtBot about getting a recommended "official" solution. For now, this works well.

I just combined the approach of doing it in test's code with doing it all in the factory definition.

Factory.define :cart_with_two_add_items, :parent => :cart do |o|
  o.after_build do |cart|
    prod_type = Factory(:add_product_type) # Define locally here and reuse below

    cart.cart_items = [Factory(:cart_item,
                               :cart => cart,
                               :product => Factory(:added_users_product,
                                                   :product_type => prod_type)),
                       Factory(:cart_item,
                               :cart => cart,
                               :product => Factory(:added_profiles_product,
                                                   :product_type => prod_type))]
  end
end

def test_cart_with_same_item_types
  cart = Factory(:cart_with_two_add_items)
  # ... Do asserts
end

I will update if I find a better solution.

Merrel answered 7/1, 2010 at 16:5 Comment(0)
O
0

Maybe you could try using factory_girl's sequences for product type name and code fields? For most tests I guess you won't care whether the product type's code is "code 1" or "sub", and for those where you care, you can always specify that explicitly.

Factory.sequence(:product_type_name) { |n| "ProductType#{n}" }
Factory.sequence(:product_type_code) { |n| "prod_#{n}" }        

Factory.define :product_type do |t|
  t.name { Factory.next(:product_type_name) }
  t.code { Factory.next(:product_type_code) }
end 
Oesophagus answered 13/1, 2010 at 23:55 Comment(1)
Thanks for the suggestion. In some cases that would be fine. However, in my case it is critical that there be only one. The purpose of the code is be used by the application so it can attribute special meaning to the ProductType in the rails code.Merrel

© 2022 - 2024 — McMap. All rights reserved.