When to use RSpec let()?

RubyRspec

Ruby Problem Overview


I tend to use before blocks to set instance variables. I then use those variables across my examples. I recently came upon let(). According to RSpec docs, it is used to

> ... to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples.

How is this different from using instance variables in before blocks? And also when should you use let() vs before()?

Ruby Solutions


Solution 1 - Ruby

I always prefer let to an instance variable for a couple of reasons:

  • Instance variables spring into existence when referenced. This means that if you fat finger the spelling of the instance variable, a new one will be created and initialized to nil, which can lead to subtle bugs and false positives. Since let creates a method, you'll get a NameError when you misspell it, which I find preferable. It makes it easier to refactor specs, too.
  • A before(:each) hook will run before each example, even if the example doesn't use any of the instance variables defined in the hook. This isn't usually a big deal, but if the setup of the instance variable takes a long time, then you're wasting cycles. For the method defined by let, the initialization code only runs if the example calls it.
  • You can refactor from a local variable in an example directly into a let without changing the referencing syntax in the example. If you refactor to an instance variable, you have to change how you reference the object in the example (e.g. add an @).
  • This is a bit subjective, but as Mike Lewis pointed out, I think it makes the spec easier to read. I like the organization of defining all my dependent objects with let and keeping my it block nice and short.

A related link can be found here: http://www.betterspecs.org/#let

Solution 2 - Ruby

The difference between using instances variables and let() is that let() is lazy-evaluated. This means that let() is not evaluated until the method that it defines is run for the first time.

The difference between before and let is that let() gives you a nice way of defining a group of variables in a 'cascading' style. By doing this, the spec looks a little better by simplifying the code.

Solution 3 - Ruby

I have completely replaced all uses of instance variables in my rspec tests to use let(). I've written a quickie example for a friend who used it to teach a small Rspec class: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

As some of the other answers here says, let() is lazy evaluated so it will only load the ones that require loading. It DRYs up the spec and make it more readable. I've in fact ported the Rspec let() code to use in my controllers, in the style of inherited_resource gem. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Along with lazy evaluation, the other advantage is that, combined with ActiveSupport::Concern, and the load-everything-in spec/support/ behavior, you can create your very own spec mini-DSL specific to your application. I've written ones for testing against Rack and RESTful resources.

The strategy I use is Factory-everything (via Machinist+Forgery/Faker). However, it is possible to use it in combination with before(:each) blocks to preload factories for an entire set of example groups, allowing the specs to run faster: http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks

Solution 4 - Ruby

It is important to keep in mind that let is lazy evaluated and not putting side-effect methods in it otherwise you would not be able to change from let to before(:each) easily. You can use let! instead of let so that it is evaluated before each scenario.

Solution 5 - Ruby

In general, let() is a nicer syntax, and it saves you typing @name symbols all over the place. But, caveat emptor! I have found let() also introduces subtle bugs (or at least head scratching) because the variable doesn't really exist until you try to use it... Tell tale sign: if adding a puts after the let() to see that the variable is correct allows a spec to pass, but without the puts the spec fails -- you have found this subtlety.

I have also found that let() doesn't seem to cache in all circumstances! I wrote it up in my blog: http://technicaldebt.com/?p=1242

Maybe it is just me?

Solution 6 - Ruby

let is functional as its essentially a Proc. Also its cached.

One gotcha I found right away with let... In a Spec block that is evaluating a change.

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

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

You'll need to be sure to call let outside of your expect block. i.e. you're calling FactoryGirl.create in your let block. I usually do this by verifying the object is persisted.

object.persisted?.should eq true

Otherwise when the let block is called the first time a change in the database will actually happen due to the lazy instantiation.

Update

Just adding a note. Be careful playing code golf or in this case rspec golf with this answer.

In this case, I just have to call some method to which the object responds. So I invoke the _.persisted?_ method on the object as its truthy. All I'm trying to do is instantiate the object. You could call empty? or nil? too. The point isn't the test but bringing the object ot life by calling it.

So you can't refactor

object.persisted?.should eq true

to be

object.should be_persisted 

as the object hasn't been instantiated... its lazy. :)

Update 2

leverage the let! syntax for instant object creation, which should avoid this issue altogether. Note though it will defeat a lot of the purpose of the laziness of the non banged let.

Also in some instances you might actually want to leverage the subject syntax instead of let as it may give you additional options.

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

Solution 7 - Ruby

Dissenting voice here: after 5 years of rspec I don't like let very much.

1. Lazy evaluation often makes test setup confusing

It becomes difficult to reason about setup when some things that have been declared in setup are not actually affecting state, while others are.

Eventually, out of frustration someone just changes let to let! (same thing without lazy evaluation) in order to get their spec working. If this works out for them, a new habit is born: when a new spec is added to an older suite and it doesn't work, the first thing the writer tries is to add bangs to random let calls.

Pretty soon all the performance benefits are gone.

2. Special syntax is unusual to non-rspec users

I would rather teach Ruby to my team than the tricks of rspec. Instance variables or method calls are useful everywhere in this project and others, let syntax will only be useful in rspec.

3. The "benefits" allow us to easily ignore good design changes

let() is good for expensive dependencies that we don't want to create over and over. It also pairs well with subject, allowing you to dry up repeated calls to multi-argument methods

Expensive dependencies repeated in many times, and methods with big signatures are both points where we could make the code better:

  • maybe I can introduce a new abstraction that isolates a dependency from the rest of my code (which would mean fewer tests need it)
  • maybe the code under test is doing too much
  • maybe I need to inject smarter objects instead of a long list of primitives
  • maybe I have a violation of tell-don't-ask
  • maybe the expensive code can be made faster (rarer - beware of premature optimisation here)

In all these cases, I can address the symptom of difficult tests with a soothing balm of rspec magic, or I can try address the cause. I feel like I spent way too much of the last few years on the former and now I want some better code.

To answer the original question: I would prefer not to, but I do still use let. I mostly use it to fit in with the style of the rest of the team (it seems like most Rails programmers in the world are now deep into their rspec magic so that is very often). Sometimes I use it when I'm adding a test to some code that I don't have control of, or don't have time to refactor to a better abstraction: i.e. when the only option is the painkiller.

Solution 8 - Ruby

Note to Joseph -- if you are creating database objects in a before(:all) they won't be captured in a transaction and you're much more likely to leave cruft in your test database. Use before(:each) instead.

The other reason to use let and its lazy evaluation is so you can take a complicated object and test individual pieces by overriding lets in contexts, as in this very contrived example:

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

Solution 9 - Ruby

"before" by default implies before(:each). Ref The Rspec Book, copyright 2010, page 228.

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

I use before(:each) to seed some data for each example group without having to call the let method to create the data in the "it" block. Less code in the "it" block in this case.

I use let if I want some data in some examples but not others.

Both before and let are great for DRYing up the "it" blocks.

To avoid any confusion, "let" is not the same as before(:all). "Let" re-evaluates its method and value for each example ("it"), but caches the value across multiple calls in the same example. You can read more about it here: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

Solution 10 - Ruby

I use let to test my HTTP 404 responses in my API specs using contexts.

To create the resource, I use let!. But to store the resource identifier, I use let. Take a look how it looks like:

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

That keeps the specs clean and readable.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionsent-hilView Question on Stackoverflow
Solution 1 - RubyMyron MarstonView Answer on Stackoverflow
Solution 2 - RubyMike LewisView Answer on Stackoverflow
Solution 3 - RubyHo-Sheng HsiaoView Answer on Stackoverflow
Solution 4 - RubyPikachuView Answer on Stackoverflow
Solution 5 - RubyJon KernView Answer on Stackoverflow
Solution 6 - RubyengineerDaveView Answer on Stackoverflow
Solution 7 - RubyiftheshoefritzView Answer on Stackoverflow
Solution 8 - RubydotdotdotPaulView Answer on Stackoverflow
Solution 9 - RubykonyakView Answer on Stackoverflow
Solution 10 - RubyVinicius BrasilView Answer on Stackoverflow