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?
You can use a before(:context)
hook to achieve this:
describe HeavyComputation, 'what I want ideally' do
before(:context) { @result = described_class.new.compute_result }
subject { @result }
it { should include 'big' }
it { should match 'string' }
it { should match /very/ }
# +50 others
end
Be aware that before(:context)
comes with a number of caveats, however:
before(:context)
It is very tempting to use before(:context)
to speed things up, but we
recommend that you avoid this as there are a number of gotchas, as well
as things that simply don't work.
before(:context)
is run in an example that is generated to provide group
context for the block.
Instance variables declared in before(:context)
are shared across all the
examples in the group. This means that each example can change the
state of a shared object, resulting in an ordering dependency that can
make it difficult to reason about failures.
RSpec has several constructs that reset state between each example
automatically. These are not intended for use from within before(:context)
:
let
declarationssubject
declarationsMock object frameworks and database transaction managers (like
ActiveRecord) are typically designed around the idea of setting up
before an example, running that one example, and then tearing down.
This means that mocks and stubs can (sometimes) be declared in
before(:context)
, but get torn down before the first real example is ever
run.
You can create database-backed model objects in a before(:context)
in
rspec-rails, but it will not be wrapped in a transaction for you, so
you are on your own to clean up in an after(:context)
block.
(from http://rubydoc.info/gems/rspec-core/RSpec/Core/Hooks:before)
As long as you understand that your before(:context)
hook is outside the normal per-example lifecycle of things like test doubles and DB transactions, and manage the necessary setup and teardown yourself explicitly, you'll be fine -- but others who work on your code base may not be aware of these gotchas.
@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.
aggregate_failures
, added in version 3.3, will do some of what you're asking about. It allows you to have multiple expectations inside of a spec, and RSpec will run each and report all failures instead of stopping at the first one.
The catch is that since you have to put it inside of a single spec, you don't get to name each expectation.
There's a block form:
it 'succeeds' do
aggregate_failures "testing response" do
expect(response.status).to eq(200)
expect(response.body).to eq('{"msg":"success"}')
end
end
And a metadata form, which applies to the whole spec:
it 'succeeds', :aggregate_failures do
expect(response.status).to eq(200)
expect(response.body).to eq('{"msg":"success"}')
end
See: https://www.relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With