ruby - practices - rspec example
¿Cuándo usar RSpec let()? (9)
Tiendo a usar bloques antes para establecer variables de instancia. Luego uso esas variables en mis ejemplos. Recientemente me encontré con let()
. Según la documentación de RSpec, se utiliza para
... para definir un método de ayuda memoized. El valor se almacenará en caché en varias llamadas en el mismo ejemplo, pero no en los ejemplos.
¿En qué se diferencia esto de usar variables de instancia en bloques anteriores? ¿Y también cuándo deberías usar let()
vs before()
?
"antes" por defecto implica before(:each)
. Ref. The Rspec Book, copyright 2010, página 228.
before(scope = :each, options={}, &block)
Uso before(:each)
para sembrar algunos datos para cada grupo de ejemplo sin tener que llamar al método let
para crear los datos en el bloque "it". Menos código en el bloque "it" en este caso.
Utilizo let
si quiero algunos datos en algunos ejemplos pero no en otros.
Tanto antes como para dejar que sean excelentes para SECAR los bloques "it".
Para evitar cualquier confusión, "dejar" no es lo mismo que before(:all)
. "Let" reevalúa su método y valor para cada ejemplo ("it"), pero almacena en caché el valor entre múltiples llamadas en el mismo ejemplo. Puede leer más sobre esto aquí: relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
En general, let()
sea una sintaxis más agradable, y le ahorra escribir símbolos @name
todas partes. ¡Pero, caveat emptor! He encontrado que let()
también introduce errores sutiles (o al menos rascarse la cabeza) porque la variable no existe realmente hasta que intentas usarla ... Indicar el signo del cuento: si se agrega un puts
después del let()
para ver eso la variable es correcta permite que pase una especificación, pero sin las puts
la especificación ha encontrado esta sutileza.
¡También encontré que let()
no parece estar en caché en todas las circunstancias! Lo escribí en mi blog: http://technicaldebt.com/?p=1242
¿Tal vez sea sólo yo?
Es importante tener en cuenta que dejar que se evalúe con pereza y no poner métodos de efectos secundarios en él, de lo contrario no sería posible cambiar de dejar a antes (: cada uno) fácilmente. Puedes usar let! En lugar de dejar que se evalúe antes de cada escenario.
He reemplazado completamente todos los usos de las variables de instancia en mis pruebas rspec para usar let (). He escrito un ejemplo rápido para un amigo que lo usó para enseñar una pequeña clase de Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html
Como dicen algunas de las otras respuestas aquí, dejemos que () se evalúe de forma perezosa, por lo que solo cargará las que requieran carga. SECA la especificación y la hace más legible. De hecho, porté el código Rspec let () para usarlo en mis controladores, en el estilo de inherited_resource gem. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html
Junto con la evaluación perezosa, la otra ventaja es que, combinada con ActiveSupport :: Concern, y la carga / todo en especificaciones / soporte / comportamiento, puede crear su propia mini-DSL de especificaciones específica para su aplicación. He escrito unos para probar los recursos de Rack y RESTful.
La estrategia que utilizo es Factory-everything (a través de Machinist + Forgery / Faker). Sin embargo, es posible usarlo en combinación con los bloques anteriores (: cada uno) para precargar fábricas para un conjunto completo de grupos de ejemplo, permitiendo que las especificaciones se ejecuten más rápido: http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks
La diferencia entre el uso de variables de instancias y let()
es que let()
se evalúa de forma perezosa . Esto significa que let()
no se evalúa hasta que el método que define se ejecute por primera vez.
La diferencia entre before
y let
es que let()
le proporciona una buena manera de definir un grupo de variables en un estilo "en cascada". Al hacer esto, la especificación se ve un poco mejor simplificando el código.
Let es funcional ya que es esencialmente un Proc. También su caché.
Una respuesta que encontré de inmediato con let ... En un bloque de especificaciones que está evaluando un cambio.
let(:object) {FactoryGirl.create :object}
expect {
post :destroy, id: review.id
}.to change(Object, :count).by(-1)
Tendrá que asegurarse de llamar a let
fuera de su bloque esperado. es decir, estás llamando a FactoryGirl.create
en tu bloque let. Usualmente hago esto verificando que el objeto es persistente.
object.persisted?.should eq true
De lo contrario, cuando se llama al bloque let
la primera vez, un cambio en la base de datos ocurrirá debido a la instanciación perezosa.
Actualizar
Sólo añadiendo una nota. Tenga cuidado al jugar golf de código o, en este caso, rspec golf con esta respuesta.
En este caso, solo tengo que llamar a algún método al que responde el objeto. Así que invoco el _.persisted?
Método del objeto como su verdad. Todo lo que estoy tratando de hacer es crear una instancia del objeto. ¿Podrías llamar vacío? o nil? también. El punto no es la prueba sino traer el objeto de la vida llamándolo.
Así que no puedes refactorizar
object.persisted?.should eq true
ser
object.should be_persisted
como el objeto no ha sido instanciado ... es perezoso. :)
Actualización 2
aprovechar el let! sintaxis para la creación instantánea de objetos, que debería evitar este problema por completo. Tenga en cuenta que esto anulará muchos de los propósitos de la pereza de los no golpeados.
También, en algunos casos, es posible que desee aprovechar la sintaxis del tema en lugar de dejarlo, ya que puede darle opciones adicionales.
subject(:object) {FactoryGirl.create :object}
Nota para Joseph: si está creando objetos de la base de datos en un before(:all)
, no se capturarán en una transacción y es mucho más probable que deje Cruft en su base de datos de prueba. Utilice before(:each)
lugar.
La otra razón para usar let y su perezosa evaluación es para que pueda tomar un objeto complicado y probar piezas individuales anulando los de los contextos, como en este ejemplo muy ingenioso:
context "foo" do
let(:params) do
{ :foo => foo, :bar => "bar" }
end
let(:foo) { "foo" }
it "is set to foo" do
params[:foo].should eq("foo")
end
context "when foo is bar" do
let(:foo) { "bar" }
# NOTE we didn''t have to redefine params entirely!
it "is set to bar" do
params[:foo].should eq("bar")
end
end
end
Siempre prefiero let
una variable de instancia por un par de razones:
- Las variables de instancia surgen cuando se hace referencia. Esto significa que si hace dedo gordo de la ortografía de la variable de instancia, se creará una nueva y se inicializará a
nil
, lo que puede llevar a errores sutiles y falsos positivos. ComoNameError
un método, obtendrás unNameError
cuando loNameError
mal, lo cual me parece preferible. También hace que sea más fácil refactorizar las especificaciones. - Un gancho
before(:each)
se ejecutará antes de cada ejemplo, incluso si el ejemplo no utiliza ninguna de las variables de instancia definidas en el gancho. Esto no suele ser un gran problema, pero si la configuración de la variable de instancia toma mucho tiempo, entonces está perdiendo ciclos. Para el método definido porlet
, el código de inicialización solo se ejecuta si el ejemplo lo llama. - Puede refactorizar desde una variable local en un ejemplo directamente a una let sin cambiar la sintaxis de referencia en el ejemplo. Si refactoriza una variable de instancia, debe cambiar la forma en que hace referencia al objeto en el ejemplo (por ejemplo, agregue una
@
). - Esto es un poco subjetivo, pero como Mike Lewis señaló, creo que hace que la especificación sea más fácil de leer. Me gusta la organización de definir todos mis objetos dependientes y
let
que mi bloque sea agradable y corto.
Utilizo let
para probar mis respuestas HTTP 404 en mis especificaciones de API usando contextos.
Para crear el recurso, utilizo let!
. Pero para almacenar el identificador de recursos, utilizo let
. Echa un vistazo a cómo se ve:
let!(:country) { create(:country) }
let(:country_id) { country.id }
before { get "api/countries/#{country_id}" }
it ''responds with HTTP 200'' { should respond_with(200) }
context ''when the country does not exist'' do
let(:country_id) { -1 }
it ''responds with HTTP 404'' { should respond_with(404) }
end
Eso mantiene las especificaciones limpias y legibles.