Singleton factories in factory_girl/machinist? - singleton

Is there some configuration in a factory of factory girl/machinist that forces it to create objects with the same factory name just once during test case and return the same instance all the time? I know, i can do something like:
def singleton name
##singletons ||= {}
##singletons[name] ||= Factory name
end
...
Factory.define :my_model do |m|
m.singleton_model { singleton :singleton_model }
end
but maybe there is a better way.

You can use the initialize_with macro inside your factory and check to see if the object already exists, then don't create it over again. This also works when said factory is referenced by associations:
FactoryGirl.define do
factory :league, :aliases => [:euro_cup] do
id 1
name "European Championship"
owner "UEFA"
initialize_with { League.find_or_create_by_id(id)}
end
end
There is a similar question here with more alternatives: Using factory_girl in Rails with associations that have unique constraints. Getting duplicate errors

#CubaLibre answer with version 5 of FactoryBot:
FactoryGirl.define do
factory :league do
initialize_with { League.find_or_initialize_by(id: id) }
sequence(:id)
name "European Championship"
end
end

Not sure if this could be useful to you.
With this setup you can create n products using the factory 'singleton_product'. All those products will have same platform (i.e. platform 'FooBar').
factory :platform do
name 'Test Platform'
end
factory :product do
name 'Test Product'
platform
trait :singleton do
platform{
search = Platform.find_by_name('FooBar')
if search.blank?
FactoryGirl.create(:platform, :name => 'FooBar')
else
search
end
}
end
factory :singleton_product, :traits => [:singleton]
end
You can still use the standard product factory 'product' to create a product with platform 'Test Platform', but it will fail when you call it to create the 2nd product (if platform name is set to be unique).

Related

Best way to modify factory attributes for multiple tests?

Trying to test some routing with rspec and factories. What would be the best way to modify an existing factory multiple times inside the spec test?
require "spec_helper"
describe gameController do
describe "routing" do
game = FactoryGirl.create(:game)
it "routes to #show" do
get("/game/1").should route_to("game#show", :id => "1")
end
it "routes to #show" do
# need to modify 1 param of the factory.. how best to do this?
get("/game/1").should route_to("game#show", :id => "1")
end
end
end
You basically have two options. If you're just modifying a single param between test, it might be easiest just to do something like:
before(:each) do
game = FactoryGirl.create(:game)
end
it "does something" do
get("/game/1").should route_to("game#show", :id => "1")
end
it "does something else" do
game.update_attributes(:param => "value")
get("/game/1").should route_to("game#show", :id => "1")
end
Otherwise, you could set up a factory girl sequence and do a fresh FactoryGirl.create in each spec.

How to skip after_build callback in factories?

I'm facing a problem while creating a factory. I have a factory like:
Factory.define :job do |j|
j.association :service_partner, :factory => :service_partner
j.price_per_task 1.to_money
end
j.after_build{|j| j.project.service_partner_ids = [j.service_partner.id] unless j.service_partner.nil?}
end
How can I skip the after_build while creating factory?
If you only want to have the after_build callback to run occasionally, your best bet is to define a nested factory:
Factory.define :job do |j|
j.association :service_partner, :factory => :service_partner
j.price_per_task 1.to_money
end
factory :job_with_additional_setup do
j.after_build{|j| j.project.service_partner_ids = [j.service_partner.id] unless j.service_partner.nil?}
end
end
You can then create a normal job by doing FactoryGirl.create(:job) or one with the after_build: FactoryGirl.create(:job_with_additional_setup)

Rails 3: As json with include option does not takes into account as_json redefinition for included association

I've got two models.
Class ModelA < ActiveRecord::Base
has_many :model_bs
end
Class ModelB < ActiveRecord::Base
belongs_to :model_a
def as_json(options = {})
{
:whatever => 'hello world'
}
end
end
When I call model_a.as_json(:include => :model_b), I want it to return a json which includes all model_bs, which it does, but employing my as_json redefinition, which it does not as it just uses the default one. Is there any way to use my own method rather than the original one? Thanks
In Rails 3, as_json method invokes serializable_hash to obtain the attributes hash. And they share the same 'options' parameter. In your case, overwritting serializable_hash would give the expected result.
def serializable_hash(options = {})
{:whatever => 'hello world'}
end
But, My suggestion is that instead of overwriting the convention, operate on the result of "super", which is like:
def serializable_hash(options = {})
hash = super
has[:name] = "hello world"
hash
end

Use the same model in two active admin classes

I'm working on an ActiveAdmin app for a large production application. I'm currently trying to use the same model for two activeadmin "entities".
So, say I have
class Person < ActiveRecord::Base
scope :special, where(:is_special => true)
scope :ordinary, where(:is_special => false)
end
Can I do something like
ActiveAdmin.register Person, :name => "Special People" do
# columns, filters for special people
controller do
def scoped_collection
Person.special
end
end
end
ActiveAdmin.register Person, :name => "Ordinary People" do
# columns, filters for ordinary people
controller do
def scoped_collection
Person.ordinary
end
end
end
(I'm making up the syntax a bit here to explain what I want to do.)
The two types of people would appear as menu items and different CRUD interfaces as defined in the ActiveAdmin.register block. They would just have the same underlying model.
Active Admin model Code:
ActiveAdmin.register Person, as: "Special People" do
scope :Special, default: true do |person|
person = Person.special
end
controller do
def scoped_collection
Person.special
end
end
end
ActiveAdmin.register Person, as: "Ordinary People" do
scope :Ordinary, default: true do |person|
person = Person.ordinary
end
controller do
def scoped_collection
Person.ordinary
end
end
end
Now in routes:
match '/admin/special_people/scoped_collection/:id' => 'admin/special_people#scoped_collection'
match '/admin/ordinary_people/scoped_collection/:id' => 'admin/ordinary_people#scoped_collection'
Try with above changes. Hope this would solve your issues. Thanks.

FactoryGirl: why does attributes_for omit some attributes?

I want to use FactoryGirl.attributes_for in controller testing, as in:
it "raise error creating a new PremiseGroup for this user" do
expect {
post :create, {:premise_group => FactoryGirl.attributes_for(:premise_group)}
}.to raise_error(CanCan::AccessDenied)
end
... but this doesn't work because #attributes_for omits the :user_id attribute. Here is the difference between #create and #attributes_for:
>> FactoryGirl.create(:premise_group)
=> #<PremiseGroup id: 3, name: "PremiseGroup_4", user_id: 6, is_visible: false, is_open: false)
>> FactoryGirl.attributes_for(:premise_group)
=> {:name=>"PremiseGroup_5", :is_visible=>false, :is_open=>false}
Note that the :user_id is absent from #attributes_for. Is this the expected behavior?
FWIW, my factories file includes definitions for :premise_group and for :user:
FactoryGirl.define do
...
factory :premise_group do
sequence(:name) {|n| "PremiseGroup_#{n}"}
user
is_visible false
is_open false
end
factory :user do
...
end
end
Short Answer:
By design, FactoryGirl's attribues_for intentionally omits things that would trigger a database transaction so tests will run fast. But you can can write a build_attributes method (below) to model all the attributes, if you're willing to take the time hit.
Original answer
Digging deep into the FactoryGirl documentation, e.g. this wiki page, you will find mentions that attributes_for ignores associations -- see update below. As a workaround, I've wrapped a helper method around FactoryGirl.build(...).attributes that strips id, created_at, and updated_at:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
end
So now:
>> build_attributes(:premise_group)
=> {"name"=>"PremiseGroup_21", "user_id"=>29, "is_visible"=>false, "is_open"=>false}
... which is exactly what's expected.
update
Having absorbed the comments from the creators of FactoryGirl, I understand why attributes_for ignores associations: referencing an association generates a call to the db which can greatly slow down tests in some cases. But if you need associations, the build_attributes approach shown above should work.
I think this is a slight improvement over fearless_fool's answer, although it depends on your desired result.
Easiest to explain with an example. Say you have lat and long attributes in your model. On your form, you don't have lat and long fields, but rather lat degree, lat minute, lat second, etc. These later can converted to the decimal lat long form.
Say your factory is like so:
factory :something
lat_d 12
lat_m 32
..
long_d 23
long_m 23.2
end
fearless's build_attributes would return { lat: nil, long: nil}. While the build_attributes below will return { lat_d: 12, lat_m: 32..., lat: nil...}
def build_attributes
ba = FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
af = FactoryGirl.attributes_for(*args)
ba.symbolize_keys.merge(af)
end
To further elaborate on the given build_attributes solution, I modified it to only add the accessible associations:
def build_attributes(*args)
obj = FactoryGirl.build(*args)
associations = obj.class.reflect_on_all_associations(:belongs_to).map { |a| "#{a.name}_id" }
accessible = obj.class.accessible_attributes
accessible_associations = obj.attributes.delete_if do |k, v|
!associations.member?(k) or !accessible.include?(k)
end
FactoryGirl.attributes_for(*args).merge(accessible_associations.symbolize_keys)
end
Here is another way:
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Limitations:
It does not generate attributes for HMT and HABTM associations (as these associations are stored in a join table, not an actual attribute).
Association strategy in the factory must be create, as in association :user, strategy: :create. This strategy can make your factory very slow if you don't use it wisely.
The accepted answer seems outdated as it did not work for me, after digging through the web & especially this Github issue, I present you:
A clean version for the most basic functionality for Rails 5+
This creates :belongs_to associations and adds their id (and type if :polymorphic) to the attributes. It also includes the code through FactoryBot::Syntax::Methods instead of an own module limited to controllers.
spec/support/factory_bot_macros.rb
module FactoryBot::Syntax::Methods
def nested_attributes_for(*args)
attributes = attributes_for(*args)
klass = args.first.to_s.camelize.constantize
klass.reflect_on_all_associations(:belongs_to).each do |r|
association = FactoryBot.create(r.class_name.underscore)
attributes["#{r.name}_id"] = association.id
attributes["#{r.name}_type"] = association.class.name if r.options[:polymorphic]
end
attributes
end
end
this is an adapted version of jamesst20 on the github issue - kudos to him 👏