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
...