Let vs. Let! vs. Instance Variables in RSpec

Jul 19, 2018 - 4 min read
Let vs. Let! vs. Instance Variables in RSpec
Share

A common source of confusion when using RSpec is whether to use let, let!, or an instance variable, for storing state.

In this article I’ll try to explain the difference between let, let!, and instance variables so you know which one to choose.

When to use let

Here’s the definition for let, right from the docs.

Use let to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples. Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked.

So what that means is that if you call let multiple times in the same example, it evaluates the let block just once (the first time it’s called).

describe GetTime do
  let(:current_time) { Time.now }

  it "gets the same time over and over again" do
    puts current_time # => 2018-07-19 09:35:29 +0300
    sleep(3)
    puts current_time # => 2018-07-19 09:35:29 +0300
  end

  it "gets the time again" do
    puts current_time # => 2018-07-19 09:35:32 +0300
  end
end

As you can see, in the first example (i.e., the first it block), even though there is a three-second delay between the two calls to current_time, the value returned is the same.

That is because the first time current_time is called, its return value is cached. Then, for all subsequent calls inside that same example block, the cached value is returned.

In other words, { Time.now } is evaluated only once per example block.

However, when you call it again in the second it block, the { Time.now } block gets re-evaluated. And, as before, the value is cached for all the subsequent calls inside that second block.

Lazy evaluation means the let block runs only if and when it is referenced. Let me try to exemplify.

describe "GetTime" do
  let(:current_time) { Time.now }

  before(:each) do
    puts Time.now # => 2018-07-19 09:45:59 +0300
  end

  it "gets the time" do
    sleep(3)
    puts current_time # => 2018-07-19 09:46:02 +0300
  end
end

When current_time is defined, the { Time.now } block is not evaluated. You can see that by comparing the value of current_time when it’s called inside the first it block with the value of Time.now in the before block.

Even though we think about let as defining a variable, it’s actually a method call. Maybe that helps you create a more meaningful mental picture for what’s going on here.

When to use let!

Here’s what the docs have to say about let! (with a bang).

You can use let! to force the method’s invocation before each example.

The difference between let, and let! is that let! is called in an implicit before block. So the result is evaluated and cached before the it block.

describe "GetTime" do
  let!(:current_time) { Time.now }

  before(:each) do
    puts Time.now # => 2018-07-19 09:57:52 +0300
  end

  it "gets the time" do
    sleep(3)
    puts current_time # => 2018-07-19 09:57:52 +0300
  end
end

So even though there is a time delay before the first call to current_time, it doesn’t matter because the value was already cached in the before block.

This behavior is useful when you need to set some state before the it block runs.

Instance variables

The problem with instance variables is that they get automatically created when referenced. So if you were to have a typo in your instance variable name, you wouldn’t get an error. The instance variable would get created and initialized with nil.

This behavior could lead to a few subtle and hard to track errors.

Note that you can turn warnings on so you get notified when there’s a typo in your variable names.

In contrast, let raises a NameError if you get the name wrong.

Another thing to keep in mind is that if you initialize an instance variable in your before block, that initialization takes place for every it block if you don’t use the variable inside that block.

So if the initialization is time-consuming, it could make your tests slower.

So there you have it, make sure you choose let when you want the lazy evaluation, let! when you do not, and an instance variable… well, I can’t find a good reason to use them in your tests.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.