All about RSpec let

This post is all about RSpec’s let method. What it’s for, when you should use it and when you should avoid it.

What does let do?

let generates a method whose return value is memoized after the first call. This is known as lazy loading because the value is not loaded into memory until the method is called. Here is an example of how let is used within an RSpec test. let will generate a method called thing which returns a new instance of Thing. You can use it the same way that you would call a normal Ruby method.

describe Thing do
let(:thing) { Thing.new }

it “does something” do
thing.do_something
end
end

Instead of using a let you could write your test using regular instance variables.

describe Thing do
it “does something” do
@thing = Thing.new
@thing.do_something
end
end

The problem with this is that you can’t share @thing between other it blocks. You could solve this problem using a before block.

describe Thing do
before do
@thing = Thing.new
end
  it “does something” do
@thing.do_something
end
  it “does something else” do
@thing.do_something_else
end
end

However, using a before block is not equivalent to using a let. A before block will set up state when you initially run the test and share that state between all of your tests. When you use let the state is reset for every it block and the value is only evaluated if it is called from within the current it block.

Why use let?

We’ve established that standard variable declaration, before blocks and let all have different characteristics. So what are the advantages of using let?

  • let helps to DRY up your tests. You can share variables between all of your examples and you can also override them in specific tests. In this example, all of your tests can use thing. You can also override thing to a different value, in a describe block.
describe Thing do
let(:thing) { Thing.new }
  it “does something” do
thing.do_something
end
  describe “different case” do
let(:thing) { Thing.new(case: "different") }
    it “does something else” do
thing.do_something_else
end
end
end
  • Lazily evaluated. before blocks can make your tests slow when they set up a lot of state because the whole before block gets called even when running a single test. If you use let, instead, then it would only set up the state required for the specific test that you’re running.
  • let can make your tests easier to read for simple examples. If you look at the examples above, you’ll probably agree that the test implementation that uses let is simpler, contains less code and is easier to read than the before block alternative.

The problem with let

Using let usually turns out well with small spec files. Problems start when your spec files become large. When you use a let in the top level describe block it becomes global to all of your tests. If you have a large spec file it can be difficult to know what is in scope because a let can be far away from the code that you’re looking at.

I’ve seen people abusing let, particularly in controller tests. One example is defining some params at the top of the spec file and then using merge! to modify params in individual tests. For example:

describe Thing do
let(:params) do
{
id: 1,
name: ‘tom’,
published: true
}
end

it “does something” do
thing.do_something
end
  describe “different case” do
it “does something else” do
params.merge!(published: false)
thing.do_something_else
end
end
end

If this pattern is repeated multiple times within the spec file it makes it really difficult to know what params is referencing.

Should you use let?

RSpec’s documentation for let actually has a warning about overusing let:

let can enhance readability when used sparingly (1,2, or maybe 3 declarations) in any given example group, but that can quickly degrade with overuse.

I completely agree with this warning and I think that RSpec users should bear this in mind. You should use let but you should use it judiciously.


Originally published at kadwill.com on January 25, 2017.