The purpose of let and the differences between let and instance variables
RSpec’s let
helper method is a way of defining values that are used in tests. Below is a typical example.
require 'rspec'
RSpec.describe User do
let(:user) { User.new }
it 'does not have an id when first instantiated' do
expect(user.id).to be nil
end
end
Another common way of setting values is to use instance variables in a before
block like in the following example.
require 'rspec'
RSpec.describe User do
before { @user = User.new }
it 'does not have an id when first instantiated' do
expect(@user.id).to be nil
end
end
There are some differences between the let
approach and the instance variable approach, with one in particular that’s quite significant.
Differences between let and instance variables
First, there’s the stylistic difference. The syntax is of course a little different between the two approaches. Instance variables are of course prefixed with @
. Some people might prefer one syntax over the other. I personally find the let
syntax ever so slightly tidier.
There are also a couple mechanical differences. Because of how instance variables work in Ruby, you can use an undefined instance variable and Ruby won’t complain. This presents a slight danger. You could for example accidentally pass some undefined instance variable to a method, meaning you’d really be passing nil
as the argument. This means you might be testing something other than the behavior you meant to test. This danger is admittedly remote though. Nonetheless, the let
helper defined not an instance variable but a new method (specifically, a memoized method—we’ll see more on this shortly), meaning that if you typo your method’s name, Ruby will most certainly complain, which is of course good.
The other mechanical difference is that let
can create values that get evaluated lazily. I personally find this to be a dangerous and bad idea, which I’ll explain below, but it is a capability that the helper offers.
Perhaps the most important difference between let
and instance variables is that instance variables, when set in a before
block, can leak from one file to another. If for example an instance variable called @customer
is set in “File A”, then “File B” can reference @customer
and get the value that was set in File A. Obviously this is bad because we want our tests to be completely deterministic and independent of one another.
How let works and the difference between let and let!
How let works
I used to assume that let
simply defines a new variable for me to use. Upon closer inspection, I learned that let
is a method that returns a method. More specifically, let
returns a memoized method, a method that only gets run once.
Since that’s perhaps kind of mind-bending, let’s take a closer look at what exactly this means.
An example method
Consider this method that 1) prints something and then 2) returns a value.
def my_name
puts 'thinking about what my name is...'
'Jason Swett'
end
puts my_name
When we run puts my_name
, we see the string that gets printed (puts 'thinking about what my name is...'
) followed by the value that gets returned by the method (Jason Swett
).
$ ruby my_name.rb
thinking about what my name is...
Jason Swett
Now let’s take a look at some let
syntax that will create the same method.
require 'rspec'
describe 'my_name' do
let(:my_name) do
puts 'thinking about what my name is...'
'Jason Swett'
end
it 'returns my name' do
puts my_name
end
end
When we run this test file and invoke the my_name
method, the same exact thing happens: the method `puts`es some text and returns my name.
$ rspec my_name_spec.rb
thinking about what my name is...
Jason Swett
.
Finished in 0.00193 seconds (files took 0.08757 seconds to load)
1 example, 0 failures
Just to make it blatantly obvious and to prove that my_name
is indeed a method call and not a variable reference, here’s a version of this file with parentheses after the method call.
require 'rspec'
describe 'my_name' do
let(:my_name) do
puts 'thinking about what my name is...'
'Jason Swett'
end
it 'returns my name' do
puts my_name() # this explicitly shows that my_name() is a method call
end
end
Memoization
Here’s a version of the test that calls my_name
twice. Even though the method gets called twice, it only actually gets evaluated once.
require 'rspec'
describe 'my_name' do
let(:my_name) do
puts 'thinking about what my name is...'
'Jason Swett'
end
it 'returns my name' do
puts my_name
puts my_name
end
end
If we run this test, we can see that the return value of my_name
gets printed twice and the thinking about what my name is...
part only gets printed once.
$ rspec my_name_spec.rb
thinking about what my name is...
Jason Swett
Jason Swett
.
Finished in 0.002 seconds (files took 0.08838 seconds to load)
1 example, 0 failures
The lazy evaluation of let vs. the immediate evaluation of let!
When we use let
, the code inside our block gets evaluated lazily. In other words, none of the code inside the block gets evaluated until we actually call the method created by our let
block.
Take a look at the following example.
require 'rspec'
describe 'let' do
let(:message) do
puts 'let block is running'
'VALUE'
end
it 'does stuff' do
puts 'start of example'
puts message
puts 'end of example'
end
end
When we run this, we’ll see start of example
first because the code inside our let
block doesn’t get evaluated until we call the message
method.
$ rspec let_example_spec.rb
start of example
let block is running
VALUE
end of example
.
Finished in 0.00233 seconds (files took 0.09836 seconds to load)
1 example, 0 failures
The “bang” version of let
, let!
, evaluates the contents of our block immediately, without waiting for the method to get called.
require 'rspec'
describe 'let!' do
let!(:message) do
puts 'let block is running'
'VALUE'
end
it 'does stuff' do
puts 'start of example'
puts message
puts 'end of example'
end
end
When we run this version, we see let block is running
appearing before start of example
.
$ rspec let_example_spec.rb
let block is running
start of example
VALUE
end of example
.
Finished in 0.00224 seconds (files took 0.09131 seconds to load)
1 example, 0 failures
I always use let!
instead of let
. I’ve never encountered a situation where the lazily-evaluated version would be helpful but I have encountered situations where the lazily-evaluated version would be subtly confusing (e.g. a let
block is saving a record to the database but it’s not abundantly clear exactly at what point in the execution sequence the record gets saved). Perhaps there’s some performance benefit to allowing the lazy evaluation but in most cases it’s probably negligible. Confusion is often more expensive than slowness anyway.
Takeaways
- The biggest advantage to using
let
over instance variables is that instance variables can leak from test to test, which isn’t true oflet
. - The difference between
let
andlet!
is that the former is lazily evaluated while the latter is immediately evaluated. - I always use the
let!
version because I find the execution path to be more easily understandable.
Thanks so much for the clear explanations! All this time I thought let was creating variables. I’d be curious to know what situations you have encountered where the lazily-evaluated version would be subtly confusing. That was kind of a cliffhanger at the end of the article. Also I thought the article would touch on let scope but maybe that’s common knowledge.
Thanks! You raise a good point. I’ve edited that part to include an example. I’ve had situations before where a let block saves a database record, and it superficially looks like the record gets saved before the tests get executed, but due to let’s lazy evaluation the record actually doesn’t exist until after the time someone might expect the record to get created, resulting in a test that behaves mysteriously differently from what’s expected.
I’m conflicted on the value of `let!` (`let` is possibly an anti-pattern given that it hides the execution path entirely).
A before block using instance variables achieves the same thing, with normal Ruby flow that is better known than `let!`.
Given that, I question the value of its existence. It seems to solve a problem created by `let` itself
I think that is something that is not clear for me in the posts, and a little confusing with the definition that let is not defining variables, but methods. The “method” that let is defining is executed only once, and in the subsequent calls for the let “variable”, the method is not executed anymore. So, it appears that let is not defining a method too, but maybe something in between the definition of a method and a variable, where the method is executed for the first time the let is called, and for the subsequent calls, the variable is accessed directly. Is that right?
Yep. “let” defines a memoized method where it runs once the first time it’s executed, and then returns the value of that execution subsequently.
FWIW I updated the post to make this part more clear/accurate.
One major difference is that both “let” and “let!” have their values automatically reset for every execution. Instance variables can leak between tests, which is the major reason they are discouraged.
And yes, the main reason “let” is lazy-evaluated is for performance. It’s particularly useful if you want to define some high-level *possible* values that you might use in a number of your tests but might not use in others. Especially in larger Rails projects with hundreds or thousands of tests, the difference between the two can be anywhere from seconds to minutes for a test suite to run.
Thanks. I updated the post to mention the instance variable leak problem.
`let!(…) { … }` is actually identical to `before { let(…){ … } }`. So I agree that from your view the control flow is clearer than using `let`. But this is not the angle I look upon the matter because the main (and here missing) advantage is the possibility to DRY up your specs by overriding your `let`s together with `context` like this:
“`
describe ‘request XY’ do
before do
perform_something(params: { id: id })
end
context ‘when id is NOT specified’ do
let(:id) { nil }
it { is_expected.to be_successful }
end
context ‘when id is out of range’ do
let(:id) { 1_000_000 }
it { is_expected.not_to be_successful }
end
context ‘when id valid’ do
let(:id) { create(:dbobject).id }
it { is_expected.to be_successful }
end
end
“`
You need to get used to this inverted style of reading your tests, but it allows you to reduce the lines of codes immensely (normally for one cross-cutting concern, but with shared examples and contexts for more than one).
If you agree upon this advantage of `let`, it starts getting tricky with `let!` and I’d suggest you to not “always use the `let!`”. Because you will maneuver yourself into situations where you’re tempted to write code like this:
“`
let!(:logged_in_user) { create(:user) }
… do your tests with the logged-in user
context ‘when the user is NOT logged-in’ do
let(:logged_in_user) { nil }
…
end
“`
As soon as this happens, your code is doomed. It’s very difficult for a reader to distinguish between `let!` and `let` if he/she needs to debug the test here. In such a case I’d rather use `before` to make it very clear that `logged_in_user` is ALWAYS and in every case initialized.
You can find the formatted answer here: https://medium.com/@schmijos/dont-prefer-let-over-let-9cd4073bfbef?sk=aad23600676c09e636a857d21ecf9b1c
What’s the value in doing that?
What about putting the before block in each context (yes, repeating the same line)?
Source code is written for developers, not for machines. Sacrificing readability is effectively taking value away from the consumer of the tests, which is the developer, not the machine.
If the abstraction is more convoluted than that (multiple inter-dependent-let), then the readability is affected more, to confuse the consumer of the tests even more. In that case, a better alternative is to write a substitute: a real object, fully tested like normal production code, with diagnostic capabilities and all affordances needed to make writing tests a more comfortable process.
In this case, the consumer of the tests as well as the writer of the tests benefit from it, effectively doubling the value and improving readability at the same time.
My input with using always let! is so that you never end up doing the let-replacement, on purpose.
The alternative approach where your eyes have to jump back-and-forth across the file is a well known pattern, it’s what happened when code was written using goto.
Nice write up on the difference between the 3 approaches!
I really like using `let` in my specs, but then I “let” it get out of control and it’s bitten me a couple times.
I now prefer to forgo the use of `let!` all together. Instead, I use a `before` block for any data that needs to be available before the spec and `let` for any on-demand instances. It just seems cleaner, and more maintainable, to me.
Thanks for putting this together, Jason! As a beginner to Ruby I’ve found this to be the most clear article I’ve read so far.
Thanks!
From your article, and my struggles, I understand that in the situation where you have a test in which you want to use inside several ‘it’ blocks, the same object, let’s say a Todo, you’d rather do a before block, in order to create one instance, reused several times ? The memoized version of the todo will be memoized in each ‘it’ block, which is not a the right use of it, if I understand this correctly.
But the reason I’ve looked into the method let is that my before(:all) is not rendering correctly, because I’ve heard you have to clean variables after, or something like that. So I use before(:each), which creates variables in each ‘it’ blocks.
I’m going in circles, here.
And what about this “danger” of leaking variables into other files ? Is that fully reported ? It looks scary.
So in my example, where you want to use the same variable that store an instance of a Todo, in several ‘it’ blocks, because you want to test several things, which way is best ?
Hey Jason, thanks great post! What are your thoughts on using `:subject` ?
Thanks. I use subject. Specifically, I use the bang version of subject!, for the same reason I use the bang version of let!.