The normal behavior of let is to essentially bind to an example block (ie, it block.) This is fine in most instances, but can present serious problems if you happen to create large objects in a let that is used in multiple examples. In such a case, an instance variable almost becomes a necessity for the sanity of a test suite.
The time will compound as such:
- let = # examples called in * object creation time
- instance = object creation time
In instances where the number of examples called in exceeds 3, this time becomes an issue quickly.
Most would say that using instance variables is heresy, but with the current state of let it seems like overly-dogmatic ravings without much studies to the contrary. Of any of the articles that I've read in support of let, the only argument is some appeal to lazy loading and scoping concerns, both of which are irrelevant in well written tests.
Lazy loading doesn't seem to have a point in testing, as it's only useful when you're not sure if you'll need the data. If that's the case, why are you bothering to build the data in the first place for that test?
If you aren't mutating your objects in your tests (hint: you shouldn't ever, stub it) then an instance variable will likely never be a problem. If it is, the coupling is not the fault of the variable, it is of the test itself.
TL;DR: Is there a known way to bind a let type statement to an alternate block area? Ideally I'd like to bind it to both describe and context blocks to mitigate this issue.
Example:
describe 'Person' do
local_bind let(:person) { Person.new(actual_data) }
context 'It has no data' do
local_bind let(:person) { Person.new(blank_data) }
# tests
end
end
In which the local_bind would bind the let to the current block context rather than the examples in which it runs. Of course this is speculatory syntax, but gives the general idea here.
This would allow us to declare an object to be used across all examples that are within the block area, which would drastically cut the ill effects of let's example only memoization.
While these issues will not tend to visibly manifest on basic objects (50 calls * 0.001 instantiation is still fairly fast) it will become glaringly visible on more expensive ones (50 calls * 0.1 = 5 seconds.)
I've seen ones that take this long before, and when they somehow make their way into spec helpers and get used everywhere on a bad performance abstraction, you can imagine how a test suite of 3,500 tests can quickly bend to its' knees. The difference observed so far was a 75% drop in time taken from 8:15 to 1:57 from a JSON issue, and when I do this let fix I estimate a drop to sub-20 seconds. Working on getting evidence of this.
before(:all)
? – Peter Alfvin