When to use RSpec let()?

127,092

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. 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

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?

Share:
127,092
sent-hil
Author by

sent-hil

http://bit.ly/Q6MIib

Updated on July 08, 2022

Comments

  • sent-hil
    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() vs before()?

  • sent-hil
    sent-hil about 13 years
    I see, is that really an advantage? The code is being run for each example regardless.
  • Mike Lewis
    Mike Lewis about 13 years
    It is easier to read IMO, and readability is a huge factor in programming languages.
  • sent-hil
    sent-hil about 13 years
    I 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/mongo‌​id/… ) use single line blocks and I don't see how not having "@" makes it easier to read.
  • Myron Marston
    Myron Marston about 13 years
    As I said, it's a bit subjective, but I find it helpful to use let to define all of the dependent objects, and to use before(: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) then before(: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
    sent-hil about 13 years
    Thanks for the example, that really helped.
  • sent-hil
    sent-hil about 13 years
    Hey 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 where users come from and will need to dig deeper.
  • Ho-Sheng Hsiao
    Ho-Sheng Hsiao about 13 years
    The 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
    Ho-Sheng Hsiao about 13 years
    The 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
    Ho-Sheng Hsiao about 13 years
    You'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
    Ho-Sheng Hsiao about 13 years
    Regarding 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
    Ho-Sheng Hsiao about 13 years
    If 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
    sent-hil about 13 years
    You 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
    David Chelimsky over 12 years
    Senthil - 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
    Gar over 12 years
    So 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 the let! method instead, but maybe it would be more explicit to put that setup in before(:each)?
  • oligan
    oligan about 12 years
    I think you'd be able to detect a misseplt instance variable by turning warnings on.
  • Myron Marston
    Myron Marston about 12 years
    Andrew 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
    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
    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
    Jacob over 10 years
    if 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
    Michael Durrant about 10 years
    +1 before(:all) bugs have wasted many days of our developers time.
  • Jwan622
    Jwan622 about 8 years
    Sorry 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
    Myron Marston about 8 years
    @Jwan622: you might start by writing one example, which has foo = Foo.new(...) and then users foo on later lines. Later, you write a new example in the same example group that also needs a Foo instantiated in the same way. At this point, you want to refactor to eliminate the duplication. You can remove the foo = Foo.new(...) lines from your examples and replace it with a let(:foo) { Foo.new(...) } w/o changing how the examples use foo. But if you refactor to before { @foo = Foo.new(...) } you also have to update references in the examples from foo to @foo.
  • Jwan622
    Jwan622 about 8 years
    @MyronMarston perfect explanation!
  • Tony
    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
    fl-web over 7 years