views:

698

answers:

7

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
A: 

Personally, you could always try emailing Thoughtbot, the guys that created it. They are very active in the Boston Ruby community and it's worth a shot.

scottschulthess
A: 

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&amp;q=associations+validates_uniqueness_of#ef22581f4cd05aa9

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

adriandz
A: 

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.

Thanks all!

Mark Eric
+2  A: 

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.

Mark Eric
A: 

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 
psyho
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.
Mark Eric
+1  A: 

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

satyajit
Excellent! Thanks for telling me about this. I'll be watching this.
Mark Eric
+4  A: 

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_instance[:add_product_type] }
  #...
end

Voila!

d2vid
Excellent solution! Thanks for sharing. It makes usage simple and elegant. Just what I needed. :)
Mark Eric
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.
lawrence