When to use RSpec let()?
Solution 1
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. Sincelet
creates a method, you'll get aNameError
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 bylet
, 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 myit
block nice and short.
A related link can be found here: http://www.betterspecs.org/#let
Solution 2
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
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
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
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?
Comments
-
sent-hil almost 2 years
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()
vsbefore()
? -
sent-hil about 13 yearsI see, is that really an advantage? The code is being run for each example regardless.
-
Mike Lewis about 13 yearsIt is easier to read IMO, and readability is a huge factor in programming languages.
-
sent-hil about 13 yearsI really like the first advantage you mentioned, but could you explain a bit more about the third one? So far the examples I've seen (mongoid specs: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ) use single line blocks and I don't see how not having "@" makes it easier to read.
-
Myron Marston about 13 yearsAs I said, it's a bit subjective, but I find it helpful to use
let
to define all of the dependent objects, and to usebefore(:each)
to setup needed configuration or any mocks/stubs needed by the examples. I prefer this to one large before hook containing all of this. Also,let(:foo) { Foo.new }
is less noisy (and more to the point) thenbefore(:each) { @foo = Foo.new }
. Here's an example of how I use it: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/… -
sent-hil about 13 yearsThanks for the example, that really helped.
-
sent-hil about 13 yearsHey Ho-Sheng, I actually read several of your blog posts before asking this question. Regarding your
# spec/friendship_spec.rb
and# spec/comment_spec.rb
example, don't you think they make it less readable? I've no idea whereusers
come from and will need to dig deeper. -
Ho-Sheng Hsiao about 13 yearsThe first dozen or so people I've shown the format to all find it much more readable, and a few of them started writing with it. I've got enough spec code now using let() that I run into some of those problems too. I find myself going to the example, and starting from the innermost example group, work myself back up. It is the same skill as using a highly meta-programmable environment.
-
Ho-Sheng Hsiao about 13 yearsThe biggest gotcha I've run into is accidentally using let(:subject) {} instead of subject {}. subject() is set up differently from let(:subject), but let(:subject) will override it.
-
Ho-Sheng Hsiao about 13 yearsYou'll probably find writing specs this way much easier and faster than reading specs like this. It helps a great deal to name the let() with obvious names -- if you are good at that, then the specs are easier to read. At the same time though, if you are having difficulty reading those examples I suggest two actions: (1) it is likely you are not skilled at reading code in general or (2) continue using @variable style in specs
-
Ho-Sheng Hsiao about 13 yearsRegarding gaining skill in reading code: the trick is to manage the level of abstractions and knowing when to "let go". When I read the declaration "has_many :comments" in an ActiveRecord, I don't process through it and remind myself of every single detail of what code that opens up to (though I have, before). Likewise, it is the same thing with reading spec code using let(). My first pass is just reading it at a high level, I don't think about how users is constructed, I just know I am testing against a bunch of users. I drill down into it only when I need to.
-
Ho-Sheng Hsiao about 13 yearsIf you can let go "drilling down" into the code, then you'll find scanning a code with let() declarations much, much faster. It is easier to pick out let() declarations when scanning code than to find @variables embedded into the code. Using @variables, I don't have a good "shape" for which lines refer to assigning to the variables and which lines refer to testing the variables. Using let(), all assignments are done with let() so you know "instantly" by the shape of the letters where your declarations are.
-
sent-hil about 13 yearsYou can make the same argument about instance variables being easier to pick out, especially since some editors, like mine (gedit) highlight instance variables. I've been using
let()
the past couple days and personally I don't see a difference, except for the first advantage Myron mentioned. And I'm not so sure about letting go and what not, maybe because I'm lazy and I like seeing code upfront without having to open up yet another file. Thanks for your comments. -
David Chelimsky over 12 yearsSenthil - it's actually not necessarily run in every example when you use let(). It's lazy, so it's only run if it's referenced. Generally speaking this doesn't matter much because the point of an example group is to have several examples run in a common context.
-
Gar over 12 yearsSo does that mean you shouldn't use
let
if you need something to be evaluated every time? e.g. I need a child model to be present in the database before some behavior is triggered on the parent model. I'm not necessarily referencing that child model in the test, because I'm testing the parent models behavior. At the moment I'm using thelet!
method instead, but maybe it would be more explicit to put that setup inbefore(:each)
? -
oligan about 12 yearsI think you'd be able to detect a misseplt instance variable by turning warnings on.
-
Myron Marston about 12 yearsAndrew Grimm: true, but warnings may generate tons of noise (i.e. from gems your using that don't run warning-free). Plus, I prefer getting a
NoMethodError
to getting a warning, but YMMV. -
Harmon about 12 years@gar - I would use a Factory (like FactoryGirl) which allows you to create those required child associations when you instantiate the parent. If you do it this way, then it doesn't really matter if you use let() or a setup block. The let() is nice if you don't need to use EVERYTHING for each test in your sub-contexts. Setup should have only what's required for each one.
-
Myron Marston almost 12 years
let
always memoizes the value for the duration of a single example. It does not memoize the value across multiple examples.before(:all)
, in contrast, allows you to re-use an initialized variable in multiple examples. -
Jacob over 10 yearsif you want to use let (as now seems to be considered best practice), but need a particular variable to be instantiated right away, that's what
let!
is designed for. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/… -
Michael Durrant about 10 years+1 before(:all) bugs have wasted many days of our developers time.
-
Jwan622 about 8 yearsSorry I don't understand the third point: "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 @)." What is a referencing syntax? Why would you refactor a local variable directly into a let?
-
Myron Marston about 8 years@Jwan622: you might start by writing one example, which has
foo = Foo.new(...)
and then usersfoo
on later lines. Later, you write a new example in the same example group that also needs aFoo
instantiated in the same way. At this point, you want to refactor to eliminate the duplication. You can remove thefoo = Foo.new(...)
lines from your examples and replace it with alet(:foo) { Foo.new(...) }
w/o changing how the examples usefoo
. But if you refactor tobefore { @foo = Foo.new(...) }
you also have to update references in the examples fromfoo
to@foo
. -
Jwan622 about 8 years@MyronMarston perfect explanation!
-
Tony almost 8 years@MyronMarston About your first point, I totally agree. Extend that point from tests to all code, and I think it's a mistake that Ruby variables spring into existence at all.
-
fl-web over 7 yearsHow to prefer
let
for testing interaction requests sequences