Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FactoryGirl in Rails - Associations w/ Unique Constraints

This question is an extension to the one raised here:

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

The answer offered has worked perfectly for me. Here's what it looks like:

# Creates a class variable for factories that should be only created once.

module FactoryGirl

  class Singleton
    @@singletons = {}

    def self.execute(factory_key)
      begin
        @@singletons[factory_key] = FactoryGirl.create(factory_key)
      rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
        # already in DB so return nil
      end

      @@singletons[factory_key]
    end
  end

end

The issue that has come up for me is when I need to manually build an association to support a polymorphic association with a uniqueness constraint in a hook. For example:

class Matchup < ActiveRecord::Base
  belongs_to :event
  belongs_to :matchupable, :polymorphic => true

  validates :event_id, :uniqueness => { :scope => [:matchupable_id, :matchupable_type] }
end

class BaseballMatchup < ActiveRecord::Base
  has_one :matchup, :as => :matchupable
end

FactoryGirl.define do
  factory :matchup do
    event { FactoryGirl::Singleton.execute(:event) }
    matchupable { FactoryGirl::Singleton.execute(:baseball_matchup) }
    home_team_record '10-5'
    away_team_record '9-6'
  end

  factory :baseball_matchup do
    home_pitcher 'Joe Bloe'
    home_pitcher_record '21-0'
    home_pitcher_era 1.92
    home_pitcher_arm 'R'
    away_pitcher 'Jack John'
    away_pitcher_record '0-21'
    away_pitcher_era 9.92
    away_pitcher_arm 'R'
    after_build do |bm|
      bm.matchup = Factory.create(:matchup, :matchupable => bm)
    end
  end
end

My current singleton implementation doesn't support calling FactoryGirl::Singleton.execute(:matchup, :matchupable => bm), only FactoryGirl::Singleton.execute(:matchup).

How would you recommend modifying the singleton factory to support a call such as FactoryGirl::Singleton.execute(:matchup, :matchupable => bm) OR FactoryGirl::Singleton.execute(:matchup)?

Because right now, the above code will throw uniqueness validation error ("Event is already taken") everytime the hook is run on factory :baseball_matchup. Ultimately, this is what needs to be fixed so that there isn't more than one matchup or baseball_matchup in the DB.

like image 606
keruilin Avatar asked Mar 13 '12 04:03

keruilin


3 Answers

As zetetic has mentioned, you can define a second parameter on your execute function to send the attributes to be used during the call to FactoryGirl.create, with a default value of an empty hash so it didn't override any of them in the case you don't use it (you don't need to check in this particular case if the attributes hash is empty).

Also notice that you don't need to define a begin..end block in this case, because there isn't anything to be done after your rescue, so you can simplify your method by defining the rescue as part of the method definition. The assignation on the case that the initialization was fine will also return the assigned value, so there is no need to explicitly access the hash again to return it. With all these changes, the code will end like:

# Creates a class variable for factories that should be only created once.

module FactoryGirl

  class Singleton
    @@singletons = {}

    def self.execute(factory_key, attrs = {})
      @@singletons[factory_key] = FactoryGirl.create(factory_key, attrs)
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
      # already in DB so return nil
    end
  end

end
like image 81
Carlos Paramio Avatar answered Nov 18 '22 12:11

Carlos Paramio


You need to do two things to make this work:

  1. Accept attributes as an argument your execute method.
  2. Key off of both the factory name and the attributes when creating the singleton factories.

Note that step 1 isn't sufficient to solve your problem. Even if you allow execute to accept attributes, the first call to execute(:matchup, attributes) will cache that result and return it any time you execute(:matchup), even if you attempt to pass different attributes to execute. That's why you also need to change what you're using as the hash key for your @@singletons hash.

Here's an implementation I tested out:

module FactoryGirl
  class Singleton
    @@singletons = {}

    def self.execute(factory_key, attributes = {})

      # form a unique key for this factory and set of attributes
      key = [factory_key.to_s, '?', attributes.to_query].join

      begin
        @@singletons[key] = FactoryGirl.create(factory_key, attributes)
      rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
        # already in DB so return nil
      end

      @@singletons[key]
    end
  end
end

The key is a string consisting of the factory name and a query string representation of the attributes hash (something like "matchup?event=6&matchupable=2"). I was able to create multiple different matchups with different attributes, but it respected the uniqueness of the event/matchupable combination.

> e = FactoryGirl.create(:event)
> bm = FactoryGirl.create(:baseball_matchup)
> m = FactoryGirl::Singleton.execute(:matchup, :event => e, :matchupable => bm)
> m.id
2
> m = FactoryGirl::Singleton.execute(:matchup, :event => e, :matchupable => bm)
> m.id
2
> f = FactoryGirl.create(:event)
> m = FactoryGirl::Singleton.execute(:matchup, :event => f, :matchupable => bm)
> m.id
3

Let me know if that doesn't work for you.

like image 33
Brandan Avatar answered Nov 18 '22 11:11

Brandan


Ruby methods can have default values for arguments, so define your singleton method with an empty default options hash:

  def self.execute(factory_key, options={})

Now you can call it both ways:

  FactoryGirl::Singleton.execute(:matchup)
  FactoryGirl::Singleton.execute(:matchup, :matchupable => bm)

within the method, test the options argument hash to see if anything hase been passed in:

if options.empty?
  # no options specified
else
  # options were specified
end
like image 35
zetetic Avatar answered Nov 18 '22 11:11

zetetic