At work, we’re deciding on our test-writing style: let
/let!
blocks like let(:arg) { 5 }
vs. instance variables defined in a setup method like @arg = 5
.
- I’ve found no advantage to
let
; but I have experienced disadvantages. - I’ve found no disadvantages to instance variables.
And so, 👍 for instance variables.
I’ve written many specs and have read the rspec docs, betterspecs.org many times.
I don’t use let
/let!
because
- the purported advantage of lazy evaluation is never actually realized. I’m most always running all the tests in a file, and so there’s no time efficiency gain;
- the API [
let
andlet!
] and their magic increase the code’s complexity and so must be balanced out by some other advantage; - Let introduces magic and apparently nondeterministic behavior which has broken my tests, and I’ve only been able to fix by converting to easy-to-understand
@-
variable instantiations. -
Let has the problem of introducing non-ruby syntax — something that looks like an automatic variable isn’t one anymore.
So for me, let
is fixing a problem I don’t have, and in doing so, introduces new problems.
Maybe you should check this out https://www.youtube.com/watch?v=d2gL6CYaYwQ
LikeLike
Could you update your comment with a “because…”?
LikeLike
The ability to define a
let
in an outer context that relies on alet
defined in a nested context can be advantageous by deferring pieces of test setup into the context that is describing the relevant behavior; and, this capability is a feature provided by lazy-evaluation. Additionally,let
lazy evaluation behavior is not, to my knowledge, intended as a performance-oriented feature — instead, because the value is memoized and cleaned up automatically for each example, it provides a reliable way to manage test state. The alternative isbefore(:each)
with instance variables, which is complicated by any shared behaviors, which can interfere with instance variables from within their own context. With shared behaviors,let
reveals another advantage because a block can be passed to a shared behavior containing definition of one or morelet
calls that provide state isolation for the given shared behavior.In the end, the built-in management of
let
state is the key advantage, and this stands in contrast to the need to think deliberately about the management of instance variables in the context of RSpec examples.LikeLiked by 5 people
I agree with James Thompson, I couldn’t have described this any better:
Sounds like you may not be making use of the potential of
let
itself.LikeLiked by 1 person
Exactly what ^^ James Thompson said. Our specs tend to have a structure like
Does this make sense? RSpec’s
let
andsubject
laziness, and the ability to callsuper()
, is what enables the nested scopes to define themselves concisely in terms of differences from the outer scopes, and makes the definition of contexts so concise and clear.LikeLike
Please also see: http://betterspecs.org/#let
LikeLike
Thanks for the code sample. I find it confusing: what object is
super()
being invoked upon? It’s too magical & implicit for my tastes. I like for my tests to read like documentation; a recipe for how to use the code.LikeLike
It’s actually super simple:
super()
works for bothlet
andsubject
and refers to the same item in the parent context.LikeLike
See, now you’re losing my vote. 🙂 When I see an unqualified
.super()
, I “know” that to meanself.super()
in Ruby. And so my inquiry as a Rubyist is, “What is self in this context?” I look for the enclosing class. In an RSpec example, it’s not clear at all what the enclosing class is. I’d first assume maybe some kind of RSpec example instance.But you’re saying that
super()
is part of thelet
framework? More unpleasant magic, IMO, and violation of Principle of Least Surprise.LikeLike
Totally agree. Live (specs) and let die.
LikeLike
Here is a reason to use
let
:I’ve made the same mistake in the following two specs using your instance variable style and a normal
let
style:Maybe you see the bug. For each style, do our specs see the bug?
Undefined instance variables falling back to nil is an unfortunate design decision we have to live with and anticipate in ruby. I usually try to avoid referencing them directly when possible.
LikeLiked by 1 person
Sweet. That’s a good reason I hadn’t thought of. Makes me never want to use instance variables. I wonder if constants could be used instead.
LikeLike
You could use constants in theory but it is going to spit out a lot of warnings. Also, unless you do something like
described_class::FOO = 1
, you would be setting a top level constant which could cause a lot of weirdness and is pretty much equivalent to just using global variables. Even if you did dodescribed_class::FOO = 1
too you would still have to be very careful because, if any of your specs don’t describe a constant (so if they doRSpec.describe "Foo") then
described_class::FOO = 1becomes
nil::FOO = 1which is just
::FOO = 1` and you have the same problem all over again.LikeLike
Hi Robb! I agree with you,
let
can cause many headaches. This is one of my favorite blog posts on the topic: https://robots.thoughtbot.com/lets-notWhere I disagree with you is your endorsement of instance variables and
before
statements. I do not use instance variables orbefore
orsubject
in my specs. I rely on plain old variables and methods to get the job done.I’ve heard Derek Prior, co-host of the Bike Shed (podcast) say something to the effect of “I’ve noticed that the closer my RSpec tests are to the way I write regular Ruby, the better.” (Sorry to Derek if I am misremembering the phrasing…but it was something like that)
To show why I think my style is clearer, I will re-write the example from above how I would do it:
Example:
Jessie Example:
I find that second example to be much easier to read. And the readability benefits increase 10x when there are many specs in a file (as there usually are) and the setup for each test is contained within each
it
block. When there is a lot of shared setup, I will throw that in a private method in the file and call the method so it is still obvious what is happening to the spec reader.LikeLiked by 1 person
Interesting that you define a private method to dry up significant duplication.
let
methods are just defining methods with a bit of memoization baked in so extracting a method is roughly equivalent I think to usinglet
. With that said, I think an ideal style starts with what you’re describing here. Inline everything into your tests and then pull out duplication once it is making it slower to evolve your tests (this comes down to personal taste).LikeLike
Yep, I like the idea of simple private functions — stateless and easy to follow.
LikeLike
IMHO subject and let are excellent tools.
subject
is a obvious way to show the object under test andlet
is also a way to show collaborators explicitly.I agree that having nested lets is a bad practice though.
I have been following http://betterspecs.org/ for some years and I really like it.
A nice thing about their website is that they provide github issues to discuss the good practices. Something that is harder in a blog post.
Nice to hear you opinion.
LikeLike
There are some business areas, where the code depends on business days. As soon as you have an extensive use of
Timecop.travel
, lazy evaluation is a must.LikeLike