Several statements for single tuning in RSpec

I have a few slower specifications that I would like to optimize. An example of such a specification is as follows:

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 

It is very readable, and I am pleased with it in general, except that each additional specification will add at least 1 second to the total duration. This is not very acceptable.

(Please do not discuss optimization in the HeavyComputation class, as it is beyond the scope of this question.)

So I have to resort to such a specification:

 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, because its launch time will always be almost constant. The problem is that crashes are very difficult to track, and reading them is not very simple.

Ideally, I would like to have a mixture of both. Something like that:

 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 do not see where to even begin to implement it. There are many potential problems associated with it (in the event that the hooks should be caused by the overall result).

What I want to know is there an existing solution to this problem. If not, what would be the best way to handle this?

+5
source share
3 answers

@Myron Marston gave some inspiration, so my first attempt to implement it in a more or less reusable way ended with the following use (note 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 process an object only once, according to the very first specification, and not in shared blocks. This makes it unnecessary to change anything (since all hooks will be completed).

Of course, shared_subject assumes a common state with all its quirks.

But each new nested context will create a new shared object and to some extent eliminate the possibility of a state leak.

More importantly, all we need to do to handle s state leaks (if they creep in) is to replace shared_subject with subject . Then you run the usual RSpec examples.

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

0
source

To achieve this, you can use before(:context) hook:

 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 

Remember that before(:context) contains a number of caveats:

Warning: before(:context)

It is very tempting to use before(:context) to speed up the process, but we recommend that you avoid this, as there are a number of errors. like things that just don't work.

Context

before(:context) run in the example that is created to provide the group context for the block.

instance variables

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 common object, which leads to an order dependency, which can make it difficult to talk about failures.

unsupported rspec constructs

RSpec has several constructs that reset define between each example automatically. They are not intended to be used from before(:context) :

  • let ads
  • subject ads
  • Any mocking, sealing, or testing double ads

other frameworks

Object mockups and database transaction managers (such as ActiveRecord) are typically designed around the idea of ​​creating before an example, by running this one example and then breaking it down. This means that layouts and stubs can (sometimes) be declared in before(:context) , but come off before the first real example works.

You can create model objects with database support in before(:context) in rspec-rails, but it will not wrap the transaction for you, so you yourself can clear the 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 life cycle of every example of things like double tests and database transactions, and you explicitly manage the necessary settings and disconnections yourself, you'll be fine, but others who work on your code base may not be aware of these errors.

+3
source

aggregate_failures , added in version 3.3, will do some of what you ask. This allows you to have many expectations within the specification, and RSpec will start each and report all failures, rather than stopping at the first.

The trick is that since you must put it in one specification, you cannot name every expectation.

There is 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 that applies to the entire specification:

 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

+1
source

Source: https://habr.com/ru/post/1201686/


All Articles