ruby on rails - has_many - Usar factory_girl en Rails con asociaciones que tienen restricciones únicas. Obteniendo errores duplicados
factorygirl ruby (10)
¿Tal vez podría intentar usar las secuencias de factory_girl para los campos de nombre y código de tipo de producto? Para la mayoría de las pruebas, supongo que no le importará si el código del tipo de producto es "código 1" o "secundario", y para aquellos en los que le importa, siempre puede especificarlo explícitamente.
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
Estoy trabajando con un proyecto Rails 2.2 trabajando para actualizarlo. Estoy reemplazando accesorios existentes con fábricas (usando factory_girl) y he tenido algunos problemas. El problema es con los modelos que representan tablas con datos de búsqueda. Cuando creo un carrito con dos productos que tienen el mismo tipo de producto, cada producto creado vuelve a crear el mismo tipo de producto. Este error proviene de una validación única en el modelo ProductType.
Demostración de problemas
Esto es de una prueba unitaria donde creo un carro y lo pongo en pedazos. Tenía que hacer esto para solucionar el problema. Esto todavía demuestra el problema sin embargo. Lo explicaré.
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))]
Los dos productos que se agregan son del mismo tipo y cuando se crea cada producto, se vuelve a crear el tipo de producto y se crean duplicados.
El error que se genera es: "ActiveRecord :: RecordInvalid: validación fallida: el nombre ya se ha tomado, el código ya se ha tomado"
Solución
La solución para este ejemplo es anular el tipo de producto que se está utilizando y pasar en una instancia específica para que solo se use una instancia. El "add_product_type" se recoge temprano y se transfiere para cada artículo del carrito.
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
Pregunta
¿Cuál es la mejor manera de usar factory_girl con tipos de asociación de "lista de selección"?
Me gustaría que la definición de fábrica contenga todo en lugar de tener que ensamblarla en la prueba, aunque puedo vivir con ella.
Fondo y detalles adicionales
fábricas / producto.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
El objetivo del "código" de ProductType es que la aplicación les dé un significado especial. El modelo de ProductType se parece a esto:
class ProductType < ActiveRecord::Base
has_many :products
validates_presence_of :name, :code
validates_uniqueness_of :name, :code
#...
end
fábricas / 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
Cuando trato de definir el carrito con dos elementos del mismo tipo de producto, obtengo el mismo error descrito anteriormente.
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
Creo que al menos encontré una manera más limpia.
Me gusta la idea de contactar a ThoughtBot para obtener una solución "oficial" recomendada. Por ahora, esto funciona bien.
Simplemente combiné el enfoque de hacerlo en el código de prueba con hacerlo todo en la definición de fábrica.
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
Actualizaré si encuentro una mejor solución.
EDITAR:
Vea una solución aún más limpia en la parte inferior de esta respuesta.
RESPUESTA ORIGINAL:
Esta es mi solución para crear asociaciones singleton de FactoryGirl:
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
Lo llamas, por ejemplo, como:
And the following platform versions exists:
| Name |
| Master |
| Slave |
| Replica |
De esta forma, las 3 versiones de plataforma tendrán la misma plataforma ''Foo'', es decir, singleton.
Si quiere guardar una consulta db, puede hacer lo siguiente:
platform {
search = Platform.find(:first)
if search.blank?
FactoryGirl.create(:platform)
else
search
end
}
Y puede considerar hacer de la asociación singleton un rasgo:
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
Si desea configurar más de 1 plataforma y tener diferentes conjuntos de platform_versions, puede crear diferentes características que son más específicas, es decir:
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
Espero que esto sea útil para algunos por ahí.
EDITAR:
Después de enviar mi solución original anterior, le di al código otra mirada, y encontré una manera aún más clara de hacer esto: no defines los rasgos en las fábricas, sino que especificas la asociación cuando llamas al paso de prueba.
Hacer fábricas regulares:
FactoryGirl.define do
factory :platform do
name ''Foo''
end
factory :platform_version do
name ''Bar''
platform
end
end
Ahora llama al paso de prueba con la asociación especificada:
And the following platform versions exists:
| Name | Platform |
| Master | Name: NewFoo |
| Slave | Name: NewFoo |
| Replica | Name: NewFoo |
Al hacerlo así, la creación de la plataforma ''NewFoo'' usa la funcionalidad ''find_or_create_by'', por lo que la primera llamada crea la plataforma, las próximas 2 llamadas encuentran la plataforma ya creada.
De esta forma, las 3 versiones de plataforma tendrán la misma plataforma ''NewFoo'', y usted podrá crear tantos conjuntos de versiones de plataforma como necesite.
Creo que esta es una solución muy limpia, ya que mantiene la fábrica limpia, e incluso hace que sea visible para el lector de sus pasos de prueba que esas 3 versiones de plataforma tienen la misma plataforma.
Encontré el mismo problema y agregué un lambda en la parte superior del archivo de mi fábrica que implementa un patrón singleton, que también regenera el modelo si el db ha sido borrado desde la última ronda de pruebas / especificaciones:
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
Luego, usando sus fábricas de ejemplo, puede usar un atributo lazy factory_girl para ejecutar el lambda
Factory.define :product do |p|
p.product_type { single_instances[:add_product_type] }
#...this block edited as per comment below
end
Voila!
Estos problemas se eliminarán cuando los singleton se introduzcan en las fábricas, se encuentra actualmente en http://github.com/roderickvd/factory_girl/tree/singletons Issue - http://github.com/thoughtbot/factory_girl/issues#issue/16
He tenido el mismo problema, y creo que es el mismo al que se hace referencia aquí: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+validates_uniqueness_of#ef22581f4cd05aa9
Creo que su solución alternativa es posiblemente la mejor solución al problema.
Inspirado por las respuestas aquí encontré la sugerencia de @Jonas Bang la más cercana a mis necesidades. Esto es lo que funcionó para mí a mediados de 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
Ejemplo de uso para crear cuatro platform_version con la misma referencia de plataforma:
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
Y si necesitabas ''Dar'' en una plataforma distinta:
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
Se siente como lo mejor de ambos mundos sin inclinar a factory_girl demasiado fuera de forma.
La respuesta corta es "no", Factory girl no tiene una manera más clara de hacerlo. Me pareció verificar esto en los foros de Factory Girl.
Sin embargo, encontré otra respuesta para mí. Implica otro tipo de solución, pero hace todo mucho más limpio.
La idea es cambiar los modelos que representan las tablas de búsqueda para crear la entrada requerida si falta. Esto está bien porque el código espera que existan entradas específicas. Aquí hay un ejemplo del modelo modificado.
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
El método de clase estática de "id_for_addition" cargará el modelo y la ID si se encuentra, si no se encuentra, la creará.
La desventaja es que el método "id_for_addition" puede no ser claro en cuanto a lo que hace por su nombre. Eso puede necesitar cambiar. El único otro impacto del código para el uso normal es una prueba adicional para ver si se encontró el modelo o no.
Esto significa que el código de fábrica para crear el producto se puede cambiar así ...
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
Esto significa que el código de Fábrica modificado puede verse así ...
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
Esto es exactamente lo que quería. Ahora puedo expresar claramente mi código de fábrica y de prueba.
Otro beneficio de este enfoque es que los datos de la tabla de búsqueda no necesitan ser sembrados o poblados en migraciones. Se manejará solo para bases de datos de prueba y producción.
Solo para tu información, también puedes usar la macro initialize_with
dentro de tu fábrica y verificar si el objeto ya existe, y luego no volver a crearlo. La solución que usa un lambda (¡es increíble, pero!) Es una lógica de replicación ya presente en find_or_create_by. Esto también funciona para asociaciones donde: la liga se está creando a través de una fábrica asociada.
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
Tuve una situación similar. Terminé usando mis semillas.rb para definir los singletons y luego requiriendo seeds.rb en el spec_helper.rb para crear los objetos en la base de datos de prueba. Entonces puedo buscar el objeto apropiado en las fábricas.
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