Rails - RSpec - Difference between "let" and "let!"
Asked Answered
S

6

53

I have read what the RSpec manual says about the difference, but some things are still confusing. Every other source, including "The RSpec Book" only explain about "let", and "The Rails 3 Way" is just as confusing as the manual.

I understand that "let" is only evaluated when invoked, and keeps the same value within a scope. So it makes sense that in the first example in the manual the first test passes as the "let" is invoked only once, and the second test passes as it adds to the value of the first test (which was evaluated once in the first test and has the value of 1).

Following that, since "let!" evaluates when defined, and again when invoked, should the test not fail as "count.should eq(1)" should have instead be "count.should eq(2)"?

Any help would be appreciated.

Stanfordstang answered 16/4, 2012 at 11:17 Comment(0)
S
15

It's not invoked when defined, but rather before each example (and then it's memoized and not invoked again by the example). This way, count will have a value of 1.

Anyway, if you have another example, the before hook is invoked again - all of the following tests pass:

$count = 0
describe "let!" do
  invocation_order = []

  let!(:count) do
    invocation_order << :let!
    $count += 1
  end

  it "calls the helper method in a before hook" do
    invocation_order << :example
    invocation_order.should == [:let!, :example]
    count.should eq(1)
  end

  it "calls the helper method again" do
    count.should eq(2)
  end
end
Stogner answered 16/4, 2012 at 15:29 Comment(5)
Agreed, which is why the first spec makes sense. Surely, though, the second spec would be evaluated before the example, (making it equal to 1) and then again when calling the "count.should" (making it equal to 2) ?Stanfordstang
No, it is already invoked and memorized, so $count is not increased again. Anyway, if you have another example, the before hook is invoked again. See my edited answer, I added some code for clarification.Stogner
So that would mean that if you use "let" and a spec (an "it") does not doa "count.should" (or similar) then the the increment will not take place? If that is the case, then "let" shouldn't be considered a "before", since before by default implies the functionality of the "let!". Or am I missing something again?Stanfordstang
it's memoize, not memorize. From Obie Fernandez book "Memoized means that the code block associated with the let is executed once and stored for future invocations, increasing performance."Tenfold
copying the doc sample code was part of the problem in the first place. And hence here didn't help very much.Story
T
69

I understood the difference between let and let! with a very simple example. Let me read the doc sentence first, then show the output hands on.

About let doc says :-

... let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked.

I understood the difference with the below example :-

$count = 0
describe "let" do
  let(:count) { $count += 1 }

  it "returns 1" do
    expect($count).to eq(1)
  end
end

Lets run it now :-

arup@linux-wzza:~/Ruby> rspec spec/test_spec.rb
F

Failures:

  1) let is not cached across examples
     Failure/Error: expect($count).to eq(1)

       expected: 1
            got: 0

       (compared using ==)
     # ./spec/test_spec.rb:8:in `block (2 levels) in <top (required)>'

Finished in 0.00138 seconds (files took 0.13618 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/test_spec.rb:7 # let is not cached across examples
arup@linux-wzza:~/Ruby>

Why the ERROR ? Because, as doc said, with let, it is not evaluated until the first time the method it defines is invoked. In the example, we didn't invoke the count, thus $count is still 0, not incremented by 1.

Now coming to the part let!. The doc is saying

....You can use let! to force the method's invocation before each example. It means even if you didn't invoke the helper method inside the example, still it will be invoked before your example runs.

Lets test this also :-

Here is the modified code

$count = 0
describe "let!" do
  let!(:count) { $count += 1 }

  it "returns 1" do
    expect($count).to eq(1)
  end
end

Lets run this code :-

arup@linux-wzza:~/Ruby> rspec spec/test_spec.rb
.

Finished in 0.00145 seconds (files took 0.13458 seconds to load)
1 example, 0 failures

See, now $count returns 1, thus test got passed. It happened as I used let!, which run before the example run, although we didn't invoke count inside our example.

This is how let and let! differs from each other.

Tellez answered 30/6, 2014 at 18:23 Comment(1)
Keep in mind, never have a let block inside of a before the block, this is what let! is made for. For more info check out this page: kolosek.com/rspec-let-vs-beforePapaverine
M
32

You can read more about this here, but basically. (:let) is lazily evaluated and will never be instantiated if you don't call it, while (:let!) is forcefully evaluated before each method call.

Mew answered 16/4, 2012 at 15:25 Comment(2)
Thanks Justin. That was the "manual" I linked to that I wasn't understanding. My question centered more on why the "2nd spec" in that link passed with "count.should eq(1)" instead of "count.should eq(2)" as I expected. If it is evaluated before the method call, and again when the "count.should" is called, shouldn't the 2nd example equal 2 instead of 1?Stanfordstang
halle-flippin-lujah, finally an explanation that doesn't bore on about memoizing or whetever that is. Simple and to the point, as everything should be.Housebreak
S
15

It's not invoked when defined, but rather before each example (and then it's memoized and not invoked again by the example). This way, count will have a value of 1.

Anyway, if you have another example, the before hook is invoked again - all of the following tests pass:

$count = 0
describe "let!" do
  invocation_order = []

  let!(:count) do
    invocation_order << :let!
    $count += 1
  end

  it "calls the helper method in a before hook" do
    invocation_order << :example
    invocation_order.should == [:let!, :example]
    count.should eq(1)
  end

  it "calls the helper method again" do
    count.should eq(2)
  end
end
Stogner answered 16/4, 2012 at 15:29 Comment(5)
Agreed, which is why the first spec makes sense. Surely, though, the second spec would be evaluated before the example, (making it equal to 1) and then again when calling the "count.should" (making it equal to 2) ?Stanfordstang
No, it is already invoked and memorized, so $count is not increased again. Anyway, if you have another example, the before hook is invoked again. See my edited answer, I added some code for clarification.Stogner
So that would mean that if you use "let" and a spec (an "it") does not doa "count.should" (or similar) then the the increment will not take place? If that is the case, then "let" shouldn't be considered a "before", since before by default implies the functionality of the "let!". Or am I missing something again?Stanfordstang
it's memoize, not memorize. From Obie Fernandez book "Memoized means that the code block associated with the let is executed once and stored for future invocations, increasing performance."Tenfold
copying the doc sample code was part of the problem in the first place. And hence here didn't help very much.Story
T
5

I also thought this was confusing, but I think the examples from The Rails 3 Way are good.
let is analogous to instance variables in the before block whereas let! is memoized immediately

From The Rails 3 Way

describe BlogPost do
  let(:blog_post) { BlogPost.create :title => 'Hello' }
  let!(:comment) { blog_post.comments.create :text => 'first post' }

  describe "#comment" do
    before do
     blog_post.comment("finally got a first post")
    end

    it "adds the comment" do
      blog_post.comments.count.should == 2
    end
  end
end

"Since the comment block would never have been executed for the first assertion if you used a let definition, only one comment would have been added in this spec even though the implementation may be working. By using let! we ensure the initial comment gets created and the spec will now pass."

Tenfold answered 4/2, 2013 at 22:25 Comment(0)
S
2

I was also confused by let and let!, so I took the documentation code from here and played with it: https://gist.github.com/3489451

Hope it helps!

Stenographer answered 28/8, 2012 at 3:44 Comment(0)
I
1

And here's a way to keep your specs predictable.

You should pretty much always use let. You should not use let! unless you intentionally want to cache the value across examples. This is why:

describe '#method' do
  # this user persists in the db across all sub contexts
  let!(:user) { create :user }

  context 'scenario 1' do
    context 'sub scenario' do
      # ...
      # 1000 lines long
      # ...
    end

    context 'sub scenario' do
      # you need to test user with a certain trait
      # and you forgot someone else (or yourself) already has a user created
      # with `let!` all the way on the top
      let(:user) { create :user, :trait }

      it 'fails even though you think it should pass' do
        # this might not be the best example but I found this pattern
        # pretty common in different code bases
        # And your spec failed, and you scratch your head until you realize
        # there are more users in the db than you like
        # and you are just testing against a wrong user
        expect(User.first.trait).to eq xxx
      end
    end
  end
end
Indebtedness answered 31/1, 2018 at 21:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.