¿Cuándo usar RSpec let ()?

447

Tiendo a usar bloques anteriores para establecer variables de instancia. Luego uso esas variables en mis ejemplos. Hace poco me encontré let(). Según los documentos de RSpec, se utiliza para

... para definir un método auxiliar memorable. El valor se almacenará en caché en varias llamadas en el mismo ejemplo, pero no en todos los ejemplos.

¿Cómo es esto diferente de usar variables de instancia en bloques anteriores? ¿Y también cuándo deberías usar let()vs before()?

enviado
fuente
1
Deje que los bloques se evalúen perezosamente, mientras que antes de que los bloques se ejecuten antes de cada ejemplo (en general son más lentos). El uso de bloques anteriores depende de la preferencia personal (estilo de codificación, simulacros / apéndices ...). Deje que los bloques sean generalmente preferidos. Puede consultar una información más detallada sobre let
Nesha Zoric
No es una buena práctica establecer variables de instancia en un enlace anterior. Echa un vistazo a betterspecs.org
Allison

Respuestas:

604

Siempre prefiero letuna variable de instancia por un par de razones:

  • Las variables de instancia surgen cuando existen referencias. Esto significa que si usted escribe mal la ortografía de la variable de instancia, se creará una nueva e inicializará nil, lo que puede conducir a errores sutiles y falsos positivos. Como letcrea un método, obtendrás un NameErrorerror cuando lo escribes mal, lo cual me parece preferible. También facilita la refactorización de las especificaciones.
  • Se before(:each)ejecutará un enlace antes de cada ejemplo, incluso si el ejemplo no usa ninguna de las variables de instancia definidas en el enlace. Esto no suele ser un gran problema, pero si la configuración de la variable de instancia lleva mucho tiempo, está desperdiciando ciclos. Para el método definido por let, el código de inicialización solo se ejecuta si el ejemplo lo llama.
  • Puede refactorizar desde una variable local en un ejemplo directamente en let sin cambiar la sintaxis de referencia en el ejemplo. Si refactoriza a una variable de instancia, debe cambiar la forma en que hace referencia al objeto en el ejemplo (por ejemplo, agregar un @).
  • Esto es un poco subjetivo, pero como señaló Mike Lewis, 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 lety mantener mi itbloque agradable y corto.

Puede encontrar un enlace relacionado aquí: http://www.betterspecs.org/#let

Myron Marston
fuente
2
Realmente me gusta la primera ventaja que mencionaste, pero ¿podrías explicar un poco más sobre la tercera? Hasta ahora, los ejemplos que he visto (especificaciones de mongoid: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ) usan bloques de una sola línea y no veo cómo no tener "@" lo hace Más fácil de leer.
enviado el
66
Como dije, es un poco subjetivo, pero me resulta útil usarlo letpara definir todos los objetos dependientes, y usarlo before(:each)para configurar la configuración necesaria o cualquier simulacro / trozo necesario por los ejemplos. Prefiero esto a un gancho grande antes que contenga todo esto. Además, let(:foo) { Foo.new }es menos ruidoso (y más al punto) entonces before(:each) { @foo = Foo.new }. Aquí hay un ejemplo de cómo lo uso: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston
Gracias por el ejemplo, eso realmente ayudó.
enviado el
3
Andrew Grimm: cierto, pero las advertencias pueden generar toneladas de ruido (es decir, de gemas que estás usando que no se ejecutan sin advertencia). Además, prefiero recibir NoMethodErroruna advertencia, pero YMMV.
Myron Marston
1
@ Jwan622: puede comenzar escribiendo un ejemplo, que tiene foo = Foo.new(...)y luego usuarios fooen líneas posteriores. Más tarde, escribe un nuevo ejemplo en el mismo grupo de ejemplo que también necesita una Fooinstancia de la misma manera. En este punto, desea refactorizar para eliminar la duplicación. Puede eliminar las foo = Foo.new(...)líneas de sus ejemplos y reemplazarlas let(:foo) { Foo.new(...) }sin cambiar el uso de los ejemplos foo. Pero si refactoriza before { @foo = Foo.new(...) }también debe actualizar las referencias en los ejemplos de fooa @foo.
Myron Marston
82

La diferencia entre usar variables de instancias y let()es que let()se evalúa de forma diferida . Esto significa que let()no se evalúa hasta que el método que define se ejecuta por primera vez.

La diferencia entre beforey letes que let()le brinda 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 al simplificar el código.

Mike Lewis
fuente
1
Ya veo, ¿es realmente una ventaja? El código se ejecuta para cada ejemplo independientemente.
enviado el
2
Es más fácil leer IMO, y la legibilidad es un factor muy importante en los lenguajes de programación.
Mike Lewis
44
Senthil: en realidad no se ejecuta necesariamente en todos los ejemplos cuando usa let (). Es vago, por lo que solo se ejecuta si se hace referencia. En términos generales, esto no importa mucho porque el objetivo de un grupo de ejemplo es tener varios ejemplos ejecutados en un contexto común.
David Chelimsky el
1
Entonces, ¿eso significa que no debes usarlo letsi necesitas evaluar algo cada vez? Por ejemplo, necesito un modelo secundario para estar presente en la base de datos antes de que se active algún comportamiento en el modelo principal. No estoy necesariamente haciendo referencia a ese modelo secundario en la prueba, porque estoy probando el comportamiento de los modelos principales. En este momento estoy usando el let!método en su lugar, pero ¿tal vez sería más explícito poner esa configuración before(:each)?
gar
2
@gar: usaría una Fábrica (como FactoryGirl) que le permite crear esas asociaciones secundarias necesarias cuando crea una instancia de la matriz. Si lo hace de esta manera, realmente no importa si usa let () o un bloque de configuración. Let () es bueno si no necesita usar TODO para cada prueba en sus subcontexto. La configuración debe tener solo lo que se requiere para cada uno.
Harmon
17

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 algunas de las otras respuestas aquí dicen, let () se evalúa de manera diferida, por lo que solo cargará las que requieren carga. SECA la especificación y la hace más legible. De hecho, he portado el código Rspec let () para usar en mis controladores, en el estilo de la joya de herencia_recursos. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Junto con la evaluación diferida, la otra ventaja es que, combinado con ActiveSupport :: Concern, y la especificación / soporte / comportamiento de cargar todo, puede crear su propia mini-DSL de especificación específica para su aplicación. He escrito algunos para probar contra Rack y recursos RESTful.

La estrategia que uso es Factory-everything (a través de Machinist + Forgery / Faker). Sin embargo, es posible usarlo en combinación con los bloques before (: each) para precargar las fábricas para un conjunto completo de grupos de ejemplo, lo que permite que las especificaciones se ejecuten más rápido: http://makandra.com/notes/770-taking-advantage -de-rspec-s-let-in-before-blocks

Ho-Sheng Hsiao
fuente
Hola Ho-Sheng, en realidad leí varias de las publicaciones de tu blog antes de hacer esta pregunta. En cuanto a tu # spec/friendship_spec.rby # spec/comment_spec.rbejemplo, ¿no crees que lo hacen menos legible? No tengo idea de dónde usersvengo y necesitaré cavar más profundo.
enviado el
A la primera docena de personas a las que les he mostrado el formato les resulta mucho más legible, y algunos de ellos comenzaron a escribir con él. Tengo suficiente código de especificación ahora usando let () que también encuentro algunos de esos problemas. Me encuentro yendo al ejemplo, y comenzando desde el grupo de ejemplo más interno, trabajo de nuevo. Es la misma habilidad que usar un entorno altamente metaprogramable.
Ho-Sheng Hsiao
2
El mayor problema con el que me he encontrado es usar accidentalmente let (: subject) {} en lugar de subject {}. subject () está configurado de manera diferente a let (: subject), pero let (: subject) lo anulará.
Ho-Sheng Hsiao
1
Si puede dejar ir "profundizando" en el código, entonces encontrará escaneando un código con declaraciones let () mucho, mucho más rápido. Es más fácil elegir declaraciones let () al escanear código que encontrar @variables incrustadas en el código. Usando @variables, no tengo una buena "forma" para qué líneas se refieren a la asignación a las variables y qué líneas se refieren a probar las variables. Usando let (), todas las tareas se realizan con let () para que sepa "instantáneamente" por la forma de las letras donde están sus declaraciones.
Ho-Sheng Hsiao
1
Puede hacer que el mismo argumento sobre las variables de instancia sea más fácil de identificar, especialmente porque algunos editores, como el mío (gedit) resaltan las variables de instancia. He estado usando let()los últimos días y personalmente no veo la diferencia, excepto por la primera ventaja que mencionó Myron. Y no estoy tan seguro de dejar ir y qué no, tal vez porque soy vago y me gusta ver el código por adelantado sin tener que abrir otro archivo. Gracias por tus comentarios.
enviado el
13

Es importante tener en cuenta que let es un vago evaluado y no incluye métodos de efectos secundarios; de lo contrario, no podrá cambiar fácilmente de let a before (: each) . Puedes usar let! en lugar de dejar que se evalúe antes de cada escenario.

pisaruk
fuente
8

En general, let()es una sintaxis más agradable y te ahorra escribir @namesímbolos por todas partes. Pero, advertencia emptor! Descubrí que let()también introduce errores sutiles (o al menos rascarse la cabeza) porque la variable realmente no existe hasta que intentas usarla ... Muestra el signo de cuento: si agregas un putsdespués let()para ver que la variable es correcta, permite una especificación para aprobar, pero sin la putsespecificación falla: ha encontrado esta sutileza.

¡También he descubierto 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?

Jon Kern
fuente
99
letsiempre memoriza el valor para la duración de un solo ejemplo. No memoriza el valor en varios ejemplos. before(:all), por el contrario, le permite reutilizar una variable inicializada en varios ejemplos.
Myron Marston el
2
si quiere usar let (como ahora parece ser considerado la mejor práctica), pero necesita una variable particular para ser instanciado de inmediato, para eso let!está diseñado. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Jacob
6

let es funcional ya que es esencialmente un Proc. También está en caché.

Un problema que encontré de inmediato con let ... En un bloque Spec que está evaluando un cambio.

let(:object) {FactoryGirl.create :object}

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)

Deberá asegurarse de llamar letfuera de su bloque esperado. es decir, está llamando FactoryGirl.createen su bloque let. Normalmente hago esto verificando que el objeto persiste.

object.persisted?.should eq true

De lo contrario, cuando letse llama al bloque por primera vez, se producirá un cambio en la base de datos debido a la instanciación diferida.

Actualizar

Solo agrego una nota. Tenga cuidado jugando código golf o en este caso rspec golf con esta respuesta.

En este caso, solo tengo que llamar a algún método al que responda el objeto. Entonces invoco el _.persisted?método _ en el objeto como verdadero. Todo lo que intento hacer es crear una instancia del objeto. ¿Podrías llamar vacío? o nulo? también. El punto no es la prueba, sino traer el objeto a la vida llamándolo.

Entonces no puedes refactorizar

object.persisted?.should eq true

ser - estar

object.should be_persisted 

como el objeto no ha sido instanciado ... es vago. :)

Actualización 2

aprovechar el let! sintaxis para la creación instantánea de objetos, lo que debería evitar este problema por completo. Tenga en cuenta, sin embargo, que derrotará mucho el propósito de la pereza del let no golpeado.

También, en algunos casos, es posible que desee aprovechar la sintaxis del tema en lugar de permitir, ya que puede ofrecerle opciones adicionales.

subject(:object) {FactoryGirl.create :object}
ingenieroDave
fuente
2

Nota para Joseph: si está creando objetos de base de datos en una before(:all), no se capturarán en una transacción y es mucho más probable que se quede en la base de datos de prueba. Usar en su before(:each)lugar.

La otra razón para usar let y su evaluación perezosa es para que pueda tomar un objeto complicado y probar piezas individuales anulando let en contextos, como en este ejemplo muy artificial:

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
dotdotdotPaul
fuente
1
Los errores de +1 antes (: todos) han desperdiciado muchos días del tiempo de nuestros desarrolladores.
Michael Durrant
1

"antes" por defecto implica before(:each). Ref. The Rspec Book, copyright 2010, página 228.

before(scope = :each, options={}, &block)

Utilizo before(:each)para sembrar algunos datos para cada grupo de ejemplo sin tener que llamar al letmétodo para crear los datos en el bloque "it". Menos código en el bloque "it" en este caso.

Lo uso letsi quiero algunos datos en algunos ejemplos pero no en otros.

Tanto antes como let son excelentes para SECAR los bloques "it".

Para evitar confusiones, "let" 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 en varias llamadas en el mismo ejemplo. Puede leer más sobre esto aquí: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

Konyak
fuente
1

Voz disidente aquí: después de 5 años de rspec no me gusta letmucho.

1. La evaluación perezosa a menudo confunde la configuración de la prueba

Se vuelve difícil razonar sobre la configuración cuando algunas cosas que se han declarado en la configuración no están afectando el estado, mientras que otras sí.

Con el tiempo, debido a la frustración alguien cambia leta let!(lo mismo sin evaluación perezosa) con el fin de conseguir su trabajo de especificaciones. Si esto funciona para ellos, nace un nuevo hábito: cuando se agrega una nueva especificación a una suite anterior y no funciona, lo primero que intenta el escritor es agregar explosiones a las letllamadas aleatorias .

Muy pronto, todos los beneficios de rendimiento se han ido.

2. La sintaxis especial es inusual para usuarios que no son rspec

Prefiero enseñar Ruby a mi equipo que los trucos de rspec. Las variables de instancia o las llamadas a métodos son útiles en todas partes en este proyecto y en otros, la letsintaxis solo será útil en rspec.

3. Los "beneficios" nos permiten ignorar fácilmente los buenos cambios de diseño

let()es bueno para las dependencias costosas que no queremos crear una y otra vez. También se combina bien subject, lo que le permite secar llamadas repetidas a métodos de múltiples argumentos

Las dependencias costosas se repiten en muchas ocasiones, y los métodos con grandes firmas son puntos en los que podríamos mejorar el código:

  • tal vez pueda introducir una nueva abstracción que aísle una dependencia del resto de mi código (lo que significaría que menos pruebas lo necesitan)
  • tal vez el código bajo prueba está haciendo demasiado
  • tal vez necesito inyectar objetos más inteligentes en lugar de una larga lista de primitivas
  • tal vez tengo una violación de decir-no-preguntar
  • tal vez el costoso código se puede hacer más rápido (más raro, tenga cuidado con la optimización prematura aquí)

En todos estos casos, puedo abordar el síntoma de las pruebas difíciles con un bálsamo calmante de magia rspec, o puedo tratar de abordar la causa. Siento que pasé demasiado de los últimos años en el primero y ahora quiero un código mejor.

Para responder a la pregunta original: preferiría no hacerlo, pero todavía lo uso let. Lo uso principalmente para encajar con el estilo del resto del equipo (parece que la mayoría de los programadores de Rails en el mundo ahora están inmersos en su magia rspec, por lo que es muy frecuente). A veces lo uso cuando agrego una prueba a algún código del que no tengo control, o no tengo tiempo para refactorizar a una mejor abstracción: es decir, cuando la única opción es el analgésico.

iftheshoefritz
fuente
0

Utilizo letpara probar mis respuestas HTTP 404 en mis especificaciones API usando contextos.

Para crear el recurso, yo uso let!. Pero para almacenar el identificador de recursos, yo uso 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.

Vinicius Brasil
fuente