tutorial rails has_many factorygirl factorybot factory_bot docs bot array ruby-on-rails ruby unit-testing fixtures factory-bot

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!




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