Quando usar o RSpec let ()?

447

Eu costumo usar os blocos anteriores para definir variáveis ​​de instância. Eu então uso essas variáveis ​​nos meus exemplos. Eu me deparei recentemente let(). De acordo com os documentos do RSpec, é usado para

... para definir um método auxiliar memorizado. O valor será armazenado em cache em várias chamadas no mesmo exemplo, mas não em exemplos.

Como isso difere do uso de variáveis ​​de instância nos blocos anteriores? E também quando você deve usar let()vs before()?

enviado
fonte
1
Permita que os blocos sejam avaliados preguiçosamente, enquanto antes os blocos são executados antes de cada exemplo (eles são mais lentos). O uso dos blocos anteriores depende da preferência pessoal (estilo de codificação, zombarias / stubs ...). Deixar blocos são geralmente preferidos. Você pode verificar a mais detalhada informação sobre let
Nesha Zoric
Não é uma boa prática definir variáveis ​​de instância em um gancho antes. Confira betterspecs.org
Allison

Respostas:

604

Eu sempre prefiro letuma variável de instância por alguns motivos:

  • As variáveis ​​de instância passam a existir quando referenciadas. Isso significa que se você dedicar a ortografia da variável de instância, uma nova será criada e inicializada nil, o que pode levar a erros sutis e falsos positivos. Como letcria um método, você receberá um NameErrorquando digita incorretamente, o que eu acho preferível. Também facilita a refatoração de especificações.
  • Um before(:each)gancho será executado antes de cada exemplo, mesmo que o exemplo não use nenhuma das variáveis ​​de instância definidas no gancho. Isso geralmente não é grande coisa, mas se a configuração da variável de instância demorar muito, você estará perdendo ciclos. Para o método definido por let, o código de inicialização é executado apenas se o exemplo o chamar.
  • Você pode refatorar de uma variável local em um exemplo diretamente para uma permissão sem alterar a sintaxe de referência no exemplo. Se você refatorar para uma variável de instância, precisará alterar como você faz referência ao objeto no exemplo (por exemplo, adicione um @).
  • Isso é um pouco subjetivo, mas, como apontou Mike Lewis, acho que facilita a leitura das especificações. Gosto da organização de definir todos os meus objetos dependentes lete manter meu itbloco agradável e curto.

Um link relacionado pode ser encontrado aqui: http://www.betterspecs.org/#let

Myron Marston
fonte
2
Eu realmente gosto da primeira vantagem que você mencionou, mas você poderia explicar um pouco mais sobre a terceira? Até agora, os exemplos que eu vi (especificações mongóides: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ) usam blocos de linha única e não vejo como não ter "@" mais fácil de ler.
enviado-hil
6
Como eu disse, é um pouco subjetivo, mas acho útil usar letpara definir todos os objetos dependentes e before(:each)para definir a configuração necessária ou quaisquer zombarias / stubs necessários nos exemplos. Eu prefiro isso a um grande anzol que contém tudo isso. Além disso, let(:foo) { Foo.new }é menos barulhento (e mais direto ao ponto) então before(:each) { @foo = Foo.new }. Aqui está um exemplo de como eu usá-lo: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/...
Myron Marston
Obrigado pelo exemplo, isso realmente ajudou.
precisa
3
Andrew Grimm: verdade, mas os avisos podem gerar muito ruído (ou seja, de gemas que você usa que não executam sem aviso). Além disso, eu prefiro NoMethodErrorreceber um aviso, mas YMMV.
Myron Marston
1
@ Jwan622: você pode começar escrevendo um exemplo, que possui foo = Foo.new(...)e depois usuários foonas linhas posteriores. Mais tarde, você escreve um novo exemplo no mesmo grupo de exemplos que também precisa ser Fooinstanciado da mesma maneira. Neste ponto, você deseja refatorar para eliminar a duplicação. Você pode remover as foo = Foo.new(...)linhas dos seus exemplos e substituí-las por uma let(:foo) { Foo.new(...) }alteração na forma como os exemplos são usados foo. Mas se você refatorar, before { @foo = Foo.new(...) }também precisará atualizar as referências nos exemplos de foopara @foo.
Myron Marston
82

A diferença entre a utilização de instâncias variáveis e let()é que let()é preguiçoso-avaliada . Isso significa que let()não é avaliado até que o método definido seja executado pela primeira vez.

A diferença entre beforee leté que let()fornece uma boa maneira de definir um grupo de variáveis ​​em um estilo 'em cascata'. Ao fazer isso, a especificação parece um pouco melhor, simplificando o código.

Mike Lewis
fonte
1
Entendo, isso é realmente uma vantagem? O código está sendo executado para cada exemplo, independentemente.
enviado-hil
2
É mais fácil ler o IMO, e a legibilidade é um fator enorme nas linguagens de programação.
Mike Lewis
4
Senthil - na verdade, não é necessariamente executado em todos os exemplos quando você usa let (). É preguiçoso, portanto, só é executado se for referenciado. De um modo geral, isso não importa muito, porque o objetivo de um grupo de exemplos é executar vários exemplos em um contexto comum.
David Chelimsky
1
Então, isso significa que você não deve usar letse precisar de algo para ser avaliado sempre? por exemplo, preciso que um modelo filho esteja presente no banco de dados antes que algum comportamento seja acionado no modelo pai. Não estou necessariamente referenciando esse modelo filho no teste, porque estou testando o comportamento dos modelos pais. No momento, estou usando o let!método, mas talvez seja mais explícito colocar essa configuração before(:each)?
gar
2
@gar - eu usaria uma Factory (como FactoryGirl) que permite criar as associações filho necessárias quando você instancia o pai. Se você fizer dessa maneira, não importa se você usa let () ou um bloco de configuração. O let () é bom se você não precisar usar TUDO para cada teste em seus sub-contextos. A instalação deve ter apenas o necessário para cada um.
Harmon
17

Substituí completamente todos os usos de variáveis ​​de instância em meus testes rspec para usar let (). Eu escrevi um exemplo rápido para um amigo que o usou para dar uma pequena aula sobre Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Como algumas das outras respostas aqui dizem, let () é avaliado preguiçosamente, portanto, ele carregará apenas os que precisam ser carregados. Seca as especificações e torna-as mais legíveis. Na verdade, eu portamos o código let () Rspec para uso em meus controladores, no estilo da gema herdated_resource. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Juntamente com a avaliação lenta, a outra vantagem é que, combinado com o ActiveSupport :: Concern e a especificação / suporte / comportamento de carregar tudo, você pode criar sua própria mini-DSL de especificação específica para seu aplicativo. Eu escrevi alguns para testar contra recursos Rack e RESTful.

A estratégia que eu uso é Factory-everything (via Machinist + Forgery / Faker). No entanto, é possível usá-lo em combinação com os blocos before (: each) para pré-carregar fábricas para um conjunto inteiro de grupos de exemplos, permitindo que as especificações funcionem mais rapidamente: http://makandra.com/notes/770-taking-advantage -de-rspec-s-deixar-antes-de-blocos

Ho-Sheng Hsiao
fonte
Ei Ho-Sheng, na verdade eu li várias postagens do seu blog antes de fazer essa pergunta. Quanto à sua # spec/friendship_spec.rbe # spec/comment_spec.rbexemplo, você não acha que eles torná-lo menos legível? Eu não tenho idéia de onde usersveio e precisará cavar mais fundo.
precisa
A primeira dúzia de pessoas que mostrei o formato o acha mais legível, e algumas começaram a escrever com ele. Agora tenho código de especificação suficiente usando let () para encontrar alguns desses problemas também. Eu me pego indo para o exemplo e, começando no grupo de exemplo mais interno, trabalho novamente. É a mesma habilidade que usar um ambiente altamente metaprogramável.
Ho-Sheng Hsiao 20/03
2
A maior pegadinha que eu encontrei é acidentalmente usando let (: subject) {} em vez do assunto {}. subject () é configurado de forma diferente de let (: subject), mas let (: subject) substitui-o.
Ho-Sheng Hsiao
1
Se você pode deixar "aprofundar" o código, encontrará a varredura de um código com as declarações let () muito, muito mais rapidamente. É mais fácil escolher as declarações let () ao verificar o código do que encontrar as variáveis ​​incorporadas no código. Usando @variables, não tenho uma boa "forma" para quais linhas se referem à atribuição às variáveis ​​e quais linhas se referem ao teste das variáveis. Usando let (), todas as atribuições são feitas com let () para que você saiba "instantaneamente" pela forma das letras em que estão suas declarações.
Ho-Sheng Hsiao
1
Você pode argumentar que as variáveis ​​de instância são mais fáceis de escolher, especialmente porque alguns editores, como o meu (gedit), destacam variáveis ​​de instância. Eu tenho usado let()os últimos dias e, pessoalmente, não vejo diferença, exceto pela primeira vantagem que Myron mencionou. E não tenho tanta certeza de deixar ir e o que não, talvez porque sou preguiçoso e gosto de ver código antecipadamente sem precisar abrir mais um arquivo. Obrigado por seus comentários.
enviado dos hil
13

É importante ter em mente que let é avaliado preguiçosamente e não coloca métodos de efeito colateral nele, caso contrário você não seria capaz de mudar de let para before (: each) facilmente. Você pode usar let! em vez de deixar, para que seja avaliado antes de cada cenário.

pisaruk
fonte
8

Em geral, let()é uma sintaxe mais agradável e economiza @namesímbolos de digitação em todo o lugar. Mas, ressalva emptor! Eu encontrei let()também introduz bugs sutis (ou pelo menos arranhões na cabeça) porque a variável realmente não existe até que você tente usá-la ... Diga sinal de conto: se adicionar um putsdepois do let()para ver se a variável está correta permite uma especificação para passar, mas sem a putsespecificação falhar - você encontrou essa sutileza.

Também descobri que let()isso não parece armazenar em cache em todas as circunstâncias! Eu escrevi no meu blog: http://technicaldebt.com/?p=1242

Talvez seja apenas eu?

Jon Kern
fonte
9
letsempre memoriza o valor pela duração de um único exemplo. Ele não memoriza o valor em vários exemplos. before(:all), por outro lado, permite reutilizar uma variável inicializada em vários exemplos.
Myron Marston
2
se você deseja usar let (como agora parece ser a melhor prática), mas precisa de uma variável específica para ser instanciada imediatamente, let!é para isso que ela foi projetada. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/...
Jacob
6

let é funcional, pois é essencialmente um Proc. Também está em cache.

Uma pegadinha que eu encontrei imediatamente com let ... Em um bloco Spec que está avaliando uma alteração.

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

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

Você precisará ligar letfora do seu bloco de espera. ou seja, você está ligando FactoryGirl.createno seu bloco let. Geralmente faço isso verificando se o objeto persiste.

object.persisted?.should eq true

Caso contrário, quando o letbloco for chamado pela primeira vez, uma alteração no banco de dados ocorrerá devido à instanciação lenta.

Atualizar

Apenas adicionando uma nota. Tenha cuidado ao jogar código golf ou, neste caso, rspec golf com esta resposta.

Nesse caso, só preciso chamar algum método ao qual o objeto responde. Então, eu invoco o _.persisted?método _ no objeto como sua verdade. Tudo o que estou tentando fazer é instanciar o objeto. Você poderia ligar em branco? ou nada? também. A questão não é o teste, mas trazer o objeto da vida, chamando-o.

Então você não pode refatorar

object.persisted?.should eq true

ser estar

object.should be_persisted 

como o objeto não foi instanciado ... é preguiçoso. :)

Atualização 2

alavancar o let! sintaxe para criação instantânea de objetos, o que deve evitar esse problema completamente. Note que isso derrotará muitos dos propósitos da preguiça do let não batido.

Além disso, em alguns casos, você pode realmente aproveitar a sintaxe do assunto em vez de deixar, pois isso pode oferecer opções adicionais.

subject(:object) {FactoryGirl.create :object}
engineerDave
fonte
2

Nota para Joseph - se você estiver criando objetos de banco de dados em um, before(:all)eles não serão capturados em uma transação e é muito mais provável que você deixe cruft em seu banco de dados de teste. Use em before(:each)vez disso.

A outra razão para usar let e sua avaliação preguiçosa é para que você possa pegar um objeto complicado e testar peças individuais substituindo let em contextos, como neste exemplo muito elaborado:

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
fonte
1
O +1 antes (: todos) dos bugs desperdiçavam muitos dias do tempo de nossos desenvolvedores.
Michael Durrant
1

"antes" por padrão implica before(:each). Ref O Livro Rspec, copyright 2010, página 228.

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

Eu uso before(:each)para propagar alguns dados para cada grupo de exemplo sem precisar chamar o letmétodo para criar os dados no bloco "it". Menos código no bloco "it" neste caso.

Eu uso letse eu quiser alguns dados em alguns exemplos, mas não em outros.

Antes e let são ótimos para secar os blocos "it".

Para evitar qualquer confusão, "let" não é o mesmo que before(:all). "Let" reavalia seu método e valor para cada exemplo ("it"), mas armazena em cache o valor em várias chamadas no mesmo exemplo. Você pode ler mais sobre isso aqui: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

konyak
fonte
1

Voz dissidente aqui: após 5 anos de rspec, não gosto letmuito.

1. A avaliação preguiçosa geralmente torna confusa a configuração do teste

Torna-se difícil argumentar sobre a instalação quando algumas coisas que foram declaradas na instalação não estão realmente afetando o estado, enquanto outras o estão.

Eventualmente, por frustração, alguém muda letpara let!(a mesma coisa sem avaliação preguiçosa) para obter as especificações funcionando. Se isso der certo para eles, nasce um novo hábito: quando uma nova especificação é adicionada a um conjunto mais antigo e não funciona, a primeira coisa que o escritor tenta é adicionar estrondos a letchamadas aleatórias .

Em breve, todos os benefícios de desempenho terão desaparecido.

2. Sintaxe especial é incomum para usuários não rspec

Prefiro ensinar Ruby ao meu time do que os truques do rspec. Variáveis ​​de instância ou chamadas de método são úteis em todos os lugares deste projeto e em outras, a letsintaxe será útil apenas no rspec.

3. Os "benefícios" nos permitem ignorar facilmente boas alterações de design

let()é bom para dependências caras que não queremos criar repetidamente. Também combina bem com subject, permitindo que você seque repetidamente chamadas para métodos com vários argumentos

Dependências caras repetidas muitas vezes e métodos com grandes assinaturas são os dois pontos em que poderíamos melhorar o código:

  • talvez eu possa introduzir uma nova abstração que isole uma dependência do resto do meu código (o que significaria menos testes necessários)
  • talvez o código em teste esteja fazendo muito
  • talvez eu precise injetar objetos mais inteligentes em vez de uma longa lista de primitivas
  • talvez eu tenha uma violação do diga-não-pergunte
  • talvez o código caro possa ser feito mais rápido (mais raro - tenha cuidado com a otimização prematura aqui)

Em todos esses casos, posso resolver o sintoma de testes difíceis com um bálsamo calmante de magia rspec, ou posso tentar resolver a causa. Sinto que passei muito dos últimos anos no primeiro e agora quero um código melhor.

Para responder à pergunta original: eu preferiria não, mas ainda uso let. Eu o uso principalmente para combinar com o estilo do resto da equipe (parece que a maioria dos programadores do Rails no mundo agora está profundamente envolvida em sua mágica rspec, o que é muito frequente). Às vezes, uso-o quando estou adicionando um teste a algum código que não tenho controle ou que não tenho tempo para refatorar para uma melhor abstração: por exemplo, quando a única opção é o analgésico.

iftheshoefritz
fonte
0

Eu uso letpara testar minhas respostas HTTP 404 em minhas especificações de API usando contextos.

Para criar o recurso, eu uso let!. Mas para armazenar o identificador de recurso, eu uso let. Veja como fica:

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

Isso mantém as especificações limpas e legíveis.

Vinicius Brasil
fonte