¿Cuándo usar RSpec let ()?


Tiendo a usar bloques antes para establecer variables de instancia. Luego utilizo esas variables en mis ejemplos. Recientemente me encontré con let(). Según RSpec docs, se utiliza para

... para definir un método helper memoizado. El valor se almacenará en caché entre varias llamadas en el mismo ejemplo, pero no entre ejemplos.

¿En qué se diferencia esto de usar variables de instancia en bloques anteriores? Y también cuando se debe utilizar let() vs before()?

 422
Author: sjakobi, 2011-03-19

9 answers

Siempre prefiero let a una variable de instancia por un par de razones:

  • Las variables de instancia surgen cuando se hace referencia a ellas. Esto significa que si usted fat dedo la ortografía de la variable de instancia, una nueva será creada e inicializada a nil, que puede conducir a errores sutiles y falsos positivos. Dado que let crea un método, obtendrás un NameError cuando lo escribas mal, lo cual me parece preferible. También hace que sea más fácil refactorizar las especificaciones.
  • Un before(:each) gancho 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á 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 de una variable local en un ejemplo directamente en un let sin cambiar la sintaxis de referencia en el ejemplo. Si refactoriza a un variable de instancia, tiene que cambiar cómo hacer referencia al objeto en el ejemplo (por ejemplo, agregar un @).
  • 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 con let y mantener mi bloque it agradable y corto.
 573
Author: Myron Marston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-08 08:00:59

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

La diferencia entre before y let es que let() le da 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.

 76
Author: Mike Lewis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-28 21:29:44

He reemplazado completamente todos los usos de 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í dice, let() es perezoso evaluado por lo que solo cargará los que requieren carga. Seca la especificación y la hace más legible. De hecho, he portado el código let() de Rspec para usar en mi controladores, al estilo de la gema inherited_resource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Junto con la evaluación perezosa, la otra ventaja es que, combinado con ActiveSupport::Concern, y el comportamiento load-everything-in spec/support/, puede crear su propio mini-DSL específico para su aplicación. He escrito unos para probar contra Rack y recursos RESTful.

La estrategia que uso es Fábrica-todo (vía Maquinista + Falsificación / Farsante). Sin embargo, es posible usarlo en combinación con bloques before(:each) para precarga de 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-of-rspec-s-let-in-before-blocks

 16
Author: Ho-Sheng Hsiao,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-19 19:19:58

Es importante tener en cuenta que let es perezoso evaluado y no poner métodos de efectos secundarios en él de lo contrario no sería capaz de cambiar de let a antes de(:cada uno) fácilmente. Puede utilizar vamos! en lugar de vamos para que sea evaluado antes de cada escenario.

 13
Author: pisaruk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-01 21:57:59

En general, let() es una sintaxis más agradable, y te ahorra escribir @name símbolos por todas partes. Pero, caveat emptor! He encontrado que let() también introduce errores sutiles (o al menos rascarse la cabeza) porque la variable realmente no existe hasta que intenta usarla... Signo Tell tale: si agregar un puts después del let() para ver que la variable es correcta permite que pase una especificación, pero sin el puts la especificación falla you ha encontrado esta sutileza.

También he encontrado que let() no parece caché en todas las circunstancias! Escribí en mi blog: http://technicaldebt.com/?p=1242

Tal vez soy solo yo?

 7
Author: Jon Kern,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-03 13:37:47

Let es funcional como su esencialmente un Proc. También está en caché.

Un gotcha lo 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ás que asegurarte de llamar a let fuera de tu bloque de espera. es decir, estás llamando FactoryGirl.create en tu bloque let. Normalmente hago esto verificando que el objeto es persistido.

object.persisted?.should eq true

De lo contrario, cuando el bloque let se llama la primera vez que se producirá un cambio en la base de datos debido al instanciación.

Update

Simplemente añadiendo una nota. Tenga cuidado jugando code 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. Así que invoco el método _.persisted?_ en el objeto como su verdad. Todo lo que estoy tratando de hacer es instanciar el objeto. ¿Podrías llamar vacío? o nil? demasiado. El punto no es la prueba sino traer vida al objeto llamándolo.

Así que no puedes refactor

object.persisted?.should eq true

Ser

object.should be_persisted 

Ya que el objeto no ha sido instanciado... es perezoso. :)

Actualización 2

Aprovechar el let! syntax para la creación instantánea de objetos, lo que debería evitar este problema por completo. Nota a pesar de que va a derrotar a una gran parte del propósito de la pereza de la no golpeado dejar.

También en algunos casos, es posible que desee aprovechar la sintaxis sujeto en lugar de dejar, ya que puede darle adicional opcion.

subject(:object) {FactoryGirl.create :object}
 5
Author: engineerDave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 02:27:12

Nota para Joseph if si está creando objetos de 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. Use before(:each) en su 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 lets 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
 2
Author: dotdotdotPaul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 02:24:02

"antes" por defecto implica before(:each). Ref El libro de Rspec, 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 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 dejar son excelentes para secar los bloques "it".

Para evitar cualquier confusión, "let" no es lo mismo que before(:all). "Let"vuelve a evaluar su método y valor para cada ejemplo ("it"), pero almacena en caché el valor en varias llamadas en el mismo ejemplo. Puedes leer más al respecto aquí: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

 1
Author: konyak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-03 01:48:20

Utilizo let para probar mis respuestas HTTP 404 en mis especificaciones de API utilizando contextos.

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

 0
Author: vnbrs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-14 11:25:42