tutorial sheet rails has_many factorybot example cheatsheet cheat bot association ruby-on-rails factory-bot

ruby-on-rails - sheet - factory bot rails tutorial



FactoryGirl build_stubbed estrategia con una asociaciĆ³n has_many (3)

TL; DR

FactoryGirl intenta ser útil al hacer una suposición muy grande cuando crea sus objetos "stub". A saber, eso: usted tiene una id , lo que significa que no es un nuevo registro, ¡y por lo tanto ya se ha mantenido!

Desafortunadamente, ActiveRecord usa esto para decidir si debe mantener la persistencia actualizada . Entonces, el modelo tropezado intenta mantener los registros en la base de datos.

Por favor, no intente calzar los talones / burlas RSpec en fábricas FactoryGirl. Al hacerlo, se mezclan dos filosofías de anotación diferentes en el mismo objeto. Elige uno o el otro.

Los simulacros de RSpec solo se deben usar durante ciertas partes del ciclo de vida de la especificación. Moverlos a la fábrica establece un entorno que ocultará la violación del diseño. Los errores que resultan de esto serán confusos y difíciles de rastrear.

Si observa la documentación para incluir RSpec en, por ejemplo, test/unit , puede ver que proporciona métodos para garantizar que los simulacros se configuren correctamente y se desarmen entre las pruebas. Poner los simulacros en las fábricas no ofrece tal garantía de que esto tenga lugar.

Hay varias opciones aquí:

  • No uses FactoryGirl para crear tus stubs; use una biblioteca de stubbing (rspec-mocks, minitest / mocks, mocha, flexmock, rr, etc.)

    Si quiere mantener su lógica de atributo de modelo en FactoryGirl, está bien. Úsalo para ese propósito y crea el código auxiliar en otro lugar:

    stub_data = attributes_for(:order) stub_data[:line_items] = Array.new(5){ double(LineItem, attributes_for(:line_item)) } order_stub = double(Order, stub_data)

    Sí, tienes que crear manualmente las asociaciones. Esto no es algo malo, ver a continuación para mayor discusión.

  • Borrar el campo de id

    after(:stub) do |order, evaluator| order.id = nil order.line_items = build_stubbed_list( :line_item, evaluator.line_items_count, order: order ) end

  • Crea tu propia definición de new_record?

    factory :order do ignore do line_items_count 1 new_record true end after(:stub) do |order, evaluator| order.define_singleton_method(:new_record?) do evaluator.new_record end order.line_items = build_stubbed_list( :line_item, evaluator.line_items_count, order: order ) end end

¿Que está pasando aqui?

OMI, en general no es una buena idea intentar crear una has_many " has_many " has_many con FactoryGirl . Esto tiende a conducir a un código más estrechamente acoplado y potencialmente muchos objetos anidados se crean innecesariamente.

Para comprender esta posición y lo que está sucediendo con FactoryGirl, debemos analizar algunas cosas:

  • La capa / persistencia de la base de datos (es decir, ActiveRecord , Mongoid , DataMapper , ROM , etc.)
  • Cualquier biblioteca de stubbing / mocking (mintest / mocks, rspec, mocha, etc.)
  • Los mocks / stubs de propósito sirven

La capa de persistencia de la base de datos

Cada capa de persistencia de la base de datos se comporta de manera diferente. De hecho, muchos se comportan de manera diferente entre las versiones principales. FactoryGirl intenta no hacer suposiciones sobre cómo se configura esa capa. Esto les da la mayor flexibilidad a largo plazo.

Suposición: supongo que está usando ActiveRecord por el resto de esta discusión.

Al momento de escribir esto, la versión GA actual de ActiveRecord es 4.1.0. Cuando configura una asociación has_many , there''s lot that goes .

Esto también es ligeramente diferente en versiones anteriores de AR. Es muy diferente en Mongoid, etc. No es razonable esperar que FactoryGirl comprenda las complejidades de todas estas gemas, ni las diferencias entre versiones. Sucede que el escritor de la asociación has_many intenta mantener la persistencia actualizada .

Usted puede estar pensando: "pero puedo configurar el inverso con un trozo"

FactoryGirl.define do factory :line_item do association :order, factory: :order, strategy: :stub end end li = build_stubbed(:line_item)

Sí, eso es cierto. Aunque es simplemente porque AR decidió no persist . Resulta que este comportamiento es algo bueno. De lo contrario, sería muy difícil configurar objetos temporales sin tocar la base de datos con frecuencia. Además, permite que varios objetos se guarden en una sola transacción, reduciendo la transacción completa si hay un problema.

Ahora, puedes estar pensando: "Puedo agregar objetos totalmente a un has_many sin llegar a la base de datos"

order = Order.new li = order.line_items.build(name: ''test'') puts LineItem.count # => 0 puts Order.count # => 0 puts order.line_items.size # => 1 li = LineItem.new(name: ''bar'') order.line_items << li puts LineItem.count # => 0 puts Order.count # => 0 puts order.line_items.size # => 2 li = LineItem.new(name: ''foo'') order.line_items.concat(li) puts LineItem.count # => 0 puts Order.count # => 0 puts order.line_items.size # => 3 order = Order.new order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") } puts LineItem.count # => 0 puts Order.count # => 0 puts order.line_items.size # => 5

Sí, pero aquí order.line_items es realmente un ActiveRecord::Associations::CollectionProxy . Define sus propios métodos build , #<< y #concat . Por supuesto, todos estos realmente delegan a la asociación definida, que para has_many son los métodos equivalentes: ActiveRecord::Associations::CollectionAssocation#build y ActiveRecord::Associations::CollectionAssocation#concat . Estos tienen en cuenta el estado actual de la instancia del modelo base para decidir si persistir ahora o más tarde.

Todo lo que FactoryGirl realmente puede hacer aquí es dejar que el comportamiento de la clase subyacente defina lo que debería suceder. De hecho, esto le permite usar FactoryGirl para generar cualquier clase , no solo modelos de bases de datos.

FactoryGirl intenta ayudar un poco para salvar objetos. Esto es principalmente en el lado de la create de las fábricas. Según su página wiki sobre la interacción con ActiveRecord :

... [una fábrica] guarda las asociaciones primero para que las claves externas se configuren correctamente en los modelos dependientes. Para crear una instancia, llama a new sin ningún argumento, asigna cada atributo (incluidas las asociaciones), y luego llama a save !. factory_girl no hace nada especial para crear instancias de ActiveRecord. No interactúa con la base de datos ni extiende ActiveRecord o sus modelos de ninguna manera.

¡Espere! Es posible que haya notado, en el ejemplo de arriba me salteó lo siguiente:

order = Order.new order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") } puts LineItem.count # => 0 puts Order.count # => 0 puts order.line_items.size # => 5

Sí, eso es correcto. ¡Podemos establecer order.line_items= en una matriz y no se conserva! Entonces, ¿qué da?

Las bibliotecas Stubbing / Mocking

Hay muchos tipos diferentes y FactoryGirl funciona con todos ellos. ¿Por qué? Porque FactoryGirl no hace nada con ninguno de ellos. No tiene conocimiento de la biblioteca que tiene.

Recuerde, agrega la sintaxis FactoryGirl a su biblioteca de prueba de su elección . No agrega su biblioteca a FactoryGirl.

Entonces, si FactoryGirl no está utilizando su biblioteca preferida, ¿qué está haciendo?

Los mocks / stubs de propósito sirven

Antes de llegar a los detalles debajo del capó, tenemos que definir what a "stub" y su propósito previsto :

Los apéndices brindan respuestas enlatadas a las llamadas realizadas durante la prueba, por lo general, no responden en absoluto a nada fuera de lo que está programado para la prueba. Los stubs también pueden registrar información sobre las llamadas, como un stub de pasarela de correo electrónico que recuerda los mensajes que "envió", o tal vez solo la cantidad de mensajes que "envió".

esto es sutilmente diferente de un "simulacro":

Simulacros ...: objetos preprogramados con expectativas que forman una especificación de las llamadas que se espera que reciban.

Los talones sirven como una forma de configurar colaboradores con respuestas enlatadas. Mantenerse solo en la API pública de los colaboradores que toca para la prueba específica mantiene los apéndices livianos y pequeños.

Sin una biblioteca de "anotaciones", puede crear fácilmente sus propios talones:

stubbed_object = Object.new stubbed_object.define_singleton_method(:name) { ''Stubbly'' } stubbed_object.define_singleton_method(:quantity) { 123 } stubbed_object.name # => ''Stubbly'' stubbed_object.quantity # => 123

Dado que FactoryGirl es completamente independiente de la biblioteca cuando se trata de sus "stubs", este es el enfoque que toman .

Si observamos la implementación de FactoryGirl v.4.4.0, podemos ver que todos los métodos siguientes se build_stubbed cuando build_stubbed :

  • persisted?
  • new_record?
  • save
  • destroy
  • connection
  • reload
  • update_attribute
  • update_column
  • craeted_at

Estos son todos muy ActiveRecord-y. Sin embargo, como has visto con has_many , es una abstracción bastante permeable. El área de superficie API activa de ActiveRecord es muy grande. No es exactamente razonable esperar que una biblioteca lo cubra por completo.

¿Por qué la asociación has_many no funciona con el código auxiliar FactoryGirl?

Como se indicó anteriormente, ActiveRecord comprueba su estado para decidir si debe mantener la persistencia actualizada . Debido a la definición new_record? de new_record? establecer cualquier has_many activará una acción de base de datos.

def new_record? id.nil? end

Antes de arrojar algunas correcciones, quiero volver a la definición de un stub :

Los apéndices brindan respuestas enlatadas a las llamadas realizadas durante la prueba, por lo general, no responden en absoluto a nada fuera de lo que está programado para la prueba . Los stubs también pueden registrar información sobre las llamadas, como un stub de pasarela de correo electrónico que recuerda los mensajes que "envió", o tal vez solo la cantidad de mensajes que "envió".

La implementación FactoryGirl de un stub viola este principio. Como no tiene idea de lo que va a hacer en su prueba / especificación, simplemente intenta evitar el acceso a la base de datos.

Solución # 1: No use FactoryGirl para crear stubs

Si desea crear / usar stubs, use una biblioteca dedicada a esa tarea. Como parece que ya está utilizando RSpec, use su característica double (y la nueva verificación instance_double , class_double , así como object_double en RSpec 3). O use Mocha, Flexmock, RR o cualquier otra cosa.

Incluso puede rodar su propia fábrica de stub súper simple (sí, hay problemas con esto, es simplemente un ejemplo de una manera fácil de hacer un objeto con respuestas enlatadas):

require ''ostruct'' def create_stub(stubbed_attributes) OpenStruct.new(stubbed_attributes) end

FactoryGirl hace que sea muy fácil crear 100 objetos modelo cuando realmente lo necesita 1. Claro, este es un problema de uso responsable; Como siempre viene el gran poder crea responsabilidad. Es muy fácil pasar por alto las asociaciones profundamente anidadas, que en realidad no pertenecen a un apéndice.

Además, como ha notado, la abstracción del "stub" de FactoryGirl es un tanto importante que lo obliga a comprender tanto su implementación como las características internas de su capa de persistencia de base de datos. Usar una libra de compilación debería liberarlo por completo de tener esta dependencia.

Si quiere mantener su lógica de atributo de modelo en FactoryGirl, está bien. Úsalo para ese propósito y crea el código auxiliar en otro lugar:

stub_data = attributes_for(:order) stub_data[:line_items] = Array.new(5){ double(LineItem, attributes_for(:line_item)) } order_stub = double(Order, stub_data)

Sí, tienes que configurar manualmente las asociaciones. Aunque solo configura las asociaciones que necesita para la prueba / especificación. No obtienes los otros 5 que no necesitas.

Esto es una cosa que tener una libra de borrado real ayuda a dejarlo explícitamente claro. Estas son sus pruebas / especificaciones que le dan retroalimentación sobre sus opciones de diseño. Con una configuración como esta, un lector de la especificación puede hacer la pregunta: "¿Por qué necesitamos 5 líneas de pedido?" Si es importante para la especificación, es genial desde el principio y obvio. De lo contrario, no debería estar allí.

Lo mismo ocurre con los que una larga cadena de métodos llamada un solo objeto, o una cadena de métodos en objetos posteriores, es probable que sea hora de detenerse. La ley de Demeter está ahí para ayudarte, no para obstaculizarte.

Arreglo # 2: Borrar el campo de id

Esto es más un truco. Sabemos que el código predeterminado establece una id . Por lo tanto, simplemente lo eliminamos.

after(:stub) do |order, evaluator| order.id = nil order.line_items = build_stubbed_list( :line_item, evaluator.line_items_count, order: order ) end

Nunca podemos tener un talón que devuelve una id Y establece una asociación has_many . La definición de new_record? que la configuración de FactoryGirl lo previene completamente.

Arreglo # 3: ¿Crea tu propia definición de new_record?

Aquí, separamos el concepto de una id de donde el stub es un new_record? . Introducimos esto en un módulo para que podamos volver a usarlo en otros lugares.

module SettableNewRecord def new_record? @new_record end def new_record=(state) @new_record = !!state end end factory :order do ignore do line_items_count 1 new_record true end after(:stub) do |order, evaluator| order.singleton_class.prepend(SettableNewRecord) order.new_record = evaluator.new_record order.line_items = build_stubbed_list( :line_item, evaluator.line_items_count, order: order ) end end

Todavía tenemos que agregarlo manualmente para cada modelo.

Dado un estándar, hay una gran relación entre dos objetos. Para un ejemplo simple, vamos con:

class Order < ActiveRecord::Base has_many :line_items end class LineItem < ActiveRecord::Base belongs_to :order end

Lo que me gustaría hacer es generar una orden trozada con una lista de elementos de línea aplazados.

FactoryGirl.define do factory :line_item do name ''An Item'' quantity 1 end end FactoryGirl.define do factory :order do ignore do line_items_count 1 end after(:stub) do |order, evaluator| order.line_items = build_stubbed_list(:line_item, evaluator.line_items_count, :order => order) end end end

El código anterior no funciona porque Rails desea llamar a guardar en el orden cuando se asigna line_items y FactoryGirl genera una excepción: RuntimeError: stubbed models are not allowed to access the database

Entonces, ¿cómo (o es posible) generar un objeto aplastado donde su colección has_may también se apaga?


He visto esta respuesta dando vueltas, pero me encontré con el mismo problema que tenía: FactoryGirl: Populate a tiene muchas relaciones que preservan la estrategia de construcción.

La forma más limpia que he encontrado es anular explícitamente las llamadas de asociación también.

require ''rspec/mocks/standalone'' FactoryGirl.define do factory :order do ignore do line_items_count 1 end after(:stub) do |order, evaluator| order.stub(line_items).and_return(FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)) end end end

¡Espero que ayude!


La solución de Bryce me pareció la más elegante, pero produce una advertencia de desaprobación sobre la nueva sintaxis de allow() .

Para usar la nueva sintaxis (limpiadora) hice esto:

ACTUALIZACIÓN 06/05/2014: mi primera propuesta fue usar un método de api privado, gracias a Aaraon K por una solución mucho mejor, por favor lea el comentario para más discusión

#spec/support/initializers/factory_girl.rb ... #this line enables us to use allow() method in factories FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods) ... #spec/factories/order_factory.rb ... FactoryGirl.define do factory :order do ignore do line_items_count 1 end after(:stub) do |order, evaluator| items = FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order) allow(order).to receive(:line_items).and_return(items) end end end ...