5
votes

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.

1
can you provide a code example of what you would ideally like to do?Eugene
Can you also clarify how you see instance variables avoiding the object creation time? Are you talking about using before(:all)?Peter Alfvin
In this case, yes. Before :all would be the most likely candidate for it. I don't like the implications and teardown, but at this stage it seems the most viable. As mentioned above, a let statement will only cache on a single example, meaning it reloads the entirety of the data for every example it is called in. If you don't mutate variables for tests, instance variables are substantially cheaper for broadly used variables.baweaver
Edited main post to reflect an example of what I was thinking.baweaver

1 Answers

-1
votes

https://github.com/rspec/rspec-core/issues/1246

I've discussed this exact issue with the RSPEC Core team, and they have no intention of clearing up this unfortunate naming convention. Instead, they seem to insist that programmers don't know any better and must have their hands held and be picked up after. Piece of work, that lot...