Dmytrii Nagirniak Dmytrii Nagirniak - 6 months ago 24
Ruby Question

Multiple assertions for single setup in RSpec

I have a few slower specs that I would like to optimise.
The example of such spec looks like:

require 'rspec'


class HeavyComputation
def compute_result
sleep 1 # something compute heavy here
"very big string"
end

end



describe HeavyComputation, 'preferred style, but slow' do

subject { described_class.new.compute_result }

it { should include 'big' }
it { should match 'string' }
it { should match /very/ }
# +50 others
end


This is very readable and I'm happy with it generally, except that every additional spec will add at least 1 second to the total run-time. That is not very acceptable.

(Please let's not discuss the optimisation on the
HeavyComputation
class as it is outside of the scope of this question).

So what I have to resort to is spec like this:

describe HeavyComputation, 'faster, but ugly' do
subject { described_class.new.compute_result }

it 'should have expected result overall' do
should include 'big'
should match 'string'
should match /very/
# +50 others
end
end


This is obviously much better performance wise because the time to run it will always be nearly constant.
The problem is that failures are very hard to track down and it is not very intuitive to read.

So ideally, I would like to have a mix of both. Something along these lines:

describe HeavyComputation, 'what I want ideally' do
with_shared_setup_or_subject_or_something_similar_with do
shared(:result) { described_class.new.compute_result }
subject { result }

it { should include 'big' }
it { should match 'string' }
it { should match /very/ }
# +50 others
end
end


But unfortunately I cannot see where to even start implementing it. There are multiple potential issues with it (should the hooks be called on shared result is among those).

What I want to know if there is an existing solution to this problem.
If no, what would be the best way to tackle it?

Answer

@Myron Marston gave some inspiration, so my first attempt to implement it in a more or less reusable way ended up with the following usage (note the shared_subject):

describe HeavyComputation do
  shared_subject { described_class.new.compute_result }

  it { should include 'big' }
  it { should match 'string' }
  it { should match /very/ }
  # +50 others
end

The idea is to only render subject once, on the very first spec instead of in the shared blocks. It makes it pretty much unnecessary to change anything (since all the hooks will be executed).

Of course shared_subject assumes the shared state with all its quirks.

But every new nested context will create a new shared subject and to some extent eliminates a possibility of a state leak.

More importantly, all we need to do in order to deal the state leaks s(should those sneak in) is to replace shared_subject back to subject. Then you're running normal RSpec examples.

I'm sure the implementation has some quirks but should be a pretty good start.