Find or create record through factory_girl association - ruby

I have a User model that belongs to a Group. Group must have unique name attribute. User factory and group factory are defined as:
Factory.define :user do |f|
f.association :group, :factory => :group
# ...
end
Factory.define :group do |f|
f.name "default"
end
When the first user is created a new group is created too. When I try to create a second user it fails because it wants to create same group again.
Is there a way to tell factory_girl association method to look first for an existing record?
Note: I did try to define a method to handle this, but then I cannot use f.association. I would like to be able to use it in Cucumber scenarios like this:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
and this can only work if association is used in Factory definition.

You can to use initialize_with with find_or_create method
FactoryGirl.define do
factory :group do
name "name"
initialize_with { Group.find_or_create_by_name(name)}
end
factory :user do
association :group
end
end
It can also be used with id
FactoryGirl.define do
factory :group do
id 1
attr_1 "default"
attr_2 "default"
...
attr_n "default"
initialize_with { Group.find_or_create_by_id(id)}
end
factory :user do
association :group
end
end
For Rails 4
The correct way in Rails 4 is Group.find_or_create_by(name: name), so you'd use
initialize_with { Group.find_or_create_by(name: name) }
instead.

I ended up using a mix of methods found around the net, one of them being inherited factories as suggested by duckyfuzz in another answer.
I did following:
# in groups.rb factory
def get_group_named(name)
# get existing group or create new one
Group.where(:name => name).first || Factory(:group, :name => name)
end
Factory.define :group do |f|
f.name "default"
end
# in users.rb factory
Factory.define :user_in_whatever do |f|
f.group { |user| get_group_named("whatever") }
end

You can also use a FactoryGirl strategy to achieve this
module FactoryGirl
module Strategy
class Find
def association(runner)
runner.run
end
def result(evaluation)
build_class(evaluation).where(get_overrides(evaluation)).first
end
private
def build_class(evaluation)
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#build_class)
end
def get_overrides(evaluation = nil)
return #overrides unless #overrides.nil?
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#evaluator).instance_variable_get(:#overrides).clone
end
end
class FindOrCreate
def initialize
#strategy = FactoryGirl.strategy_by_name(:find).new
end
delegate :association, to: :#strategy
def result(evaluation)
found_object = #strategy.result(evaluation)
if found_object.nil?
#strategy = FactoryGirl.strategy_by_name(:create).new
#strategy.result(evaluation)
else
found_object
end
end
end
end
register_strategy(:find, Strategy::Find)
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
You can use this gist.
And then do the following
FactoryGirl.define do
factory :group do
name "name"
end
factory :user do
association :group, factory: :group, strategy: :find_or_create, name: "name"
end
end
This is working for me, though.

I had a similar problem and came up with this solution. It looks for a group by name and if it is found it associates the user with that group. Otherwise it creates a group by that name and then associates with it.
factory :user do
group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
end
I hope this can be useful to someone :)

To ensure FactoryBot's build and create still behaves as it should, we should only override the logic of create, by doing:
factory :user do
association :group, factory: :group
# ...
end
factory :group do
to_create do |instance|
instance.id = Group.find_or_create_by(name: instance.name).id
instance.reload
end
name { "default" }
end
This ensures build maintains it's default behavior of "building/initializing the object" and does not perform any database read or write so it's always fast. Only logic of create is overridden to fetch an existing record if exists, instead of attempting to always create a new record.
I wrote an article explaining this.

I was looking for a way that doesn't affect the factories. Creating a Strategy is the way to go, as pointed out by #Hiasinho. However, that solution didn't work for me anymore, probably the API changed. Came up with this:
module FactoryBot
module Strategy
# Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
# Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
class FindOrCreate
def initialize
#build_strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#build_strategy
def result(evaluation)
attributes = attributes_shared_with_build_result(evaluation)
evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
end
private
# Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
# For example, devise's User model is given a `password` and generates an `encrypted_password`
# In this case, we shouldn't use `password` in the `where` clause
def attributes_shared_with_build_result(evaluation)
object_attributes = evaluation.object.attributes
evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
end
end
end
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
And use it like this:
org = FactoryBot.find_or_create(:organization, name: 'test-org')
user = FactoryBot.find_or_create(:user, email: 'test#test.com', password: 'test', organization: org)

Usually I just make multiple factory definitions. One for a user with a group and one for a groupless user:
Factory.define :user do |u|
u.email "email"
# other attributes
end
Factory.define :grouped_user, :parent => :user do |u|
u.association :group
# this will inherit the attributes of :user
end
THen you can use these in your step definitions to create users and groups seperatly and join them together at will. For example you could create one grouped user and one lone user and join the lone user to the grouped users team.
Anyway, you should take a look at the pickle gem which will allow you to write steps like:
Given a user exists with email: "hello#email.com"
And a group exists with name: "default"
And the user: "hello#gmail.com" has joined that group
When somethings happens....

I'm using exactly the Cucumber scenario you described in your question:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
You can extend it like:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
| foo#email.com | Name: mygroup |
| bar#email.com | Name: mygroup |
This will create 3 users with the group "mygroup". As it used like this uses 'find_or_create_by' functionality, the first call creates the group, the next two calls finds the already created group.

Another way to do it (that will work with any attribute and work with associations):
# config/initializers/factory_bot.rb
#
# Example use:
#
# factory :my_factory do
# change_factory_to_find_or_create
#
# some_attr { 7 }
# other_attr { "hello" }
# end
#
# FactoryBot.create(:my_factory) # creates
# FactoryBot.create(:my_factory) # finds
# FactoryBot.create(:my_factory, other_attr: "new value") # creates
# FactoryBot.create(:my_factory, other_attr: "new value") # finds
module FactoryBotEnhancements
def change_factory_to_find_or_create
to_create do |instance|
# Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
instance.attributes = attributes.except('id')
instance.id = attributes['id'] # id can't be mass-assigned
instance.instance_variable_set('#new_record', false) # marks record as persisted
end
end
end
# This makes the module available to all factory definition blocks
class FactoryBot::DefinitionProxy
include FactoryBotEnhancements
end
The only caveat is that you can't find by nil values. Other than that, it works like a dream

Related

FactoryBot is not creating the associated models list in the after create callback

I have two factories as follows:
FactoryBot.define do
factory :proofread_document do
factory :proofread_document_with_paragraphs do
after(:create) {|instance| create_list(:paragraph, 5, proofread_document: instance) }
end
end
end
FactoryBot.define do
factory :paragraph do
level { 1 }
association :proofread_document
end
end
In my RSpec test:
describe '#number_of_paragraphs_for' do
let(:proofread_document) { create(:proofread_document_with_paragraphs)}
it 'returns the number of paragraphs for the given level' do
expect(proofread_document.number_of_paragraphs_for("level_1")).to eq(1)
end
end
The test fails because there are no paragraphs:
proofead_document.paragraphs
=> []
Why are the associated paragraph objects not being created?
Associations are not magically reloaded on existing instances. This is not due to FactoryBot, but to ActiveRecord itself.
# example with activerecord:
class Foo
has_many :bars
end
class Bar
belongs_to :foo
end
foo = Foo.first
foo.bars
# => []
3.times { Bar.create(foo: foo) }
foo.bars
# => []
foo.reload.bars
# => [<#Bar ...>, <#Bar ...>, <#Bar ...>]
So you just need to reload the record (or just the association)
after(:create) do |inst|
create_list(...)
inst.paragraphs.reload
# or inst.reload
end
I found the problem.
In my paragraphs model I had placed a default scope as follows:
default_scope :minimum_word_count, ->{ where(proofread_word_count: MINIMUM_LEVEL_DATA_WORD_COUNT..Float::INFINITY)}
This caused some issues as the paragraph I was saving in my tests had a word count too low for the parameters defined in this scope.
#P.Boro and #rewritten helped by getting me to re-check my models and scopes.

Factory Girl building and creating 3 related models, association is lost after build stage

I have the following models
class Company
has_many :admins, class_name: 'Profile::CompanyAdmin'
validates :must_have_at_least_one_admin_validation
def must_have_at_least_one_admin_validation
errors.add(:admins, :not_enough) if admins.size.zero?
end
end
class Profile::CompanyAdmin
belongs_to :company
belongs_to :user, inverse_of: :company_admin_profiles
end
class User
has_many :company_admin_profiles, inverse_of: :user
end
I am trying to set up factories so I can easily build coherent data. Especially, I want to be able to create(:company, *traits) and it creates an Admin profile with a user account
factory :company do
transient do
# need one admin to pass validation
admins_count 1 # Admins with a user account
invited_admins_count 0 # Admins without user account
end
after(:build) do |comp, evaluator|
# Creating a company admin with a user
comp.admins += build_list(:company_admin_user,
evaluator.admins_count,
company: comp
).map { |u| u.company_admin_profiles.first }
comp.admins += build_list(:company_admin_profile,
evaluator.invited_admins_count,
company: comp
)
comp.entities = build_list(:entity,
evaluator.entity_count,
company: comp
)
# If I debug here, I have
# comp.admins.first.user # => Exists !
end
after(:create) do |comp, evaluator|
# If I debug here
# comp.admins.first.user # => Gone 😱
# First save users of admin profiles (we need the user ID for the admin profile user foreign key)
comp.admins.map(&:user).each(&:save!)
# Then save admins themselves
comp.admins.each(&:save!)
end
In the example above, when I debug at the end of the company after_build stage, I have successfully built admin profiles with thier users, however after the beginning of the after_create stage, I have lost the associated user in the admin profiles (cf comments)
What's wrong ?
For the reference here are the other factories for Profile/User
factory(:company_admin_user) do
transient do
company { build(:company, admins_count: 0) }
end
after(:build) do |user, evaluator|
user.company_admin_profiles << build(:company_admin_profile,
company: evaluator.company,
user: user,
)
end
after(:create) do |user, evaluator|
user.rh_profiles.each(&:save!)
end
end
factory :company_admin_profile, class: Profile::CompanyAdmin do
company
user nil # By default creating a CompanyAdmin profile does not create associated user
end
EDIT :
A simpler way to see the problem
company = FactoryGirl.build(:company)
company.admins.first.user # => Exists !
company.save # => true
company.admins.first.user # => Nil !
It would seem saving the company model first loses the 2-level deep nested user association. SO instead of
after(:create) do |comp, evaluator|
# First save the users
comp.admins.map(&:user).compact.each(&:save!)
# Then save admins themselves
comp.admins.each(&:save!)
The following does work (still not quite sure why though)
before(:create) do |comp, evaluator|
# Need to save newly created 2-level deep nested users first
comp.admins.map(&:user).compact.each(&:save!)
end
after(:create) do |comp, evaluator|
# Then save admins themselves
comp.admins.each(&:save!)
end

FactoryGirl.create doesn't save Mongoid relations

I have two classes:
class User
include Mongoid::Document
has_one :preference
attr_accessible :name
field :name, type: String
end
class Preference
include Mongoid::Document
belongs_to :user
attr_accessible :somepref
field :somepref, type: Boolean
end
And I have two factories:
FactoryGirl.define do
factory :user do
preference
name 'John'
end
end
FactoryGirl.define do
factory :preference do
somepref true
end
end
After I create a User both documents are saved in the DB, but Preference document is missing user_id field and so has_one relation doesn't work when I read User from the DB.
I've currently fixed it by adding this piece of code in User factory:
after(:create) do |user|
#user.preference.save! #without this user_id field doesn't get saved
end
Can anyone explain to me why is this happening and is there a better fix?
Mongoid seems to be lacking support here.
When FactoryGirl creates a user, it first has to create the preference for that new user. As the new user does not have an id yet, the preference can't store it either.
In general, when you try create parent & child models in one operation, you need two steps:
create the parent, persist to database so it get's an id.
create the child for the parent and persist it.
Step two would end up in an after(:create) block. Like this:
FactoryGirl.define do
factory :user do
name 'John'
after(:create) do |user|
preference { create(:preference, user: user) }
end
end
end
As stated in this answer:
To ensure that you can always immediately read back the data you just
wrote using Mongoid, you need to set the database session options
consistency: :strong, safe: true
neither of which are the default.

FactoryGirl override attribute of associated object

This is probably silly simple but I can't find an example anywhere.
I have two factories:
FactoryGirl.define do
factory :profile do
user
title "director"
bio "I am very good at things"
linked_in "http://my.linkedin.profile.com"
website "www.mysite.com"
city "London"
end
end
FactoryGirl.define do
factory :user do |u|
u.first_name {Faker::Name.first_name}
u.last_name {Faker::Name.last_name}
company 'National Stock Exchange'
u.email {Faker::Internet.email}
end
end
What I want to do is override some of the user attributes when I create a profile:
p = FactoryGirl.create(:profile, user: {email: "test#test.com"})
or something similar, but I can't get the syntax right. Error:
ActiveRecord::AssociationTypeMismatch: User(#70239688060520) expected, got Hash(#70239631338900)
I know I can do this by creating the user first and then associating it with the profile, but I thought there must be a better way.
Or this will work:
p = FactoryGirl.create(:profile, user: FactoryGirl.create(:user, email: "test#test.com"))
but this seems overly complex. Is there not a simpler way to override an associated attribute?
What is the correct syntax for this??
According to one of FactoryGirl's creators, you can't pass dynamic arguments to the association helper (Pass parameter in setting attribute on association in FactoryGirl).
However, you should be able to do something like this:
FactoryGirl.define do
factory :profile do
transient do
user_args nil
end
user { build(:user, user_args) }
after(:create) do |profile|
profile.user.save!
end
end
end
Then you can call it almost like you wanted:
p = FactoryGirl.create(:profile, user_args: {email: "test#test.com"})
I think you can make this work with callbacks and transient attributes. If you modify your profile factory like so:
FactoryGirl.define do
factory :profile do
user
ignore do
user_email nil # by default, we'll use the value from the user factory
end
title "director"
bio "I am very good at things"
linked_in "http://my.linkedin.profile.com"
website "www.mysite.com"
city "London"
after(:create) do |profile, evaluator|
# update the user email if we specified a value in the invocation
profile.user.email = evaluator.user_email unless evaluator.user_email.nil?
end
end
end
then you should be able to invoke it like this and get the desired result:
p = FactoryGirl.create(:profile, user_email: "test#test.com")
I haven't tested it, though.
Solved it by creating User first, and then Profile:
my_user = FactoryGirl.create(:user, user_email: "test#test.com")
my_profile = FactoryGirl.create(:profile, user: my_user.id)
So, this is almost the same as in the question, split across two lines.
Only real difference is the explicit access to ".id".
Tested with Rails 5.

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.
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
You need to do two things to make this work:
Accept attributes as an argument your execute method.
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.
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

Resources