RSpec中單個設置的多個斷言

[英]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.

這是非常可讀的,我對它很滿意,除了每個額外的規范將在總運行時間中添加至少1秒。這不太可接受。

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

(請不要討論HeavyComputation類的優化,因為它超出了本問題的范圍)。

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?

如果現有解決方案存在問題,我想知道什么。如果不是,那么解決它的最佳方法是什么?

3 个解决方案

#1


3  

You can use a before(:context) hook to achieve this:

您可以使用before(:context)鈎子來實現此目的:

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:

請注意,在(:context)之前會有許多警告:

Warning: 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)來加快速度是非常誘人的,但是我們建議你避免這種情況,因為有很多陷阱,以及根本不起作用的東西。

context

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

before(:context)在生成的示例中運行,以提供塊的組上下文。

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 shared object, resulting in an ordering dependency that can make it difficult to reason about failures.

在before(:context)中聲明的實例變量在組中的所有示例之間共享。這意味着每個示例都可以更改共享對象的狀態,從而導致排序依賴性,這使得很難推斷出失敗。

unsupported rspec constructs

RSpec has several constructs that reset state between each example automatically. These are not intended for use from within before(:context):

RSpec有幾個構造可以自動重置每個示例之間的狀態。這些不適合在before(:context)之內使用:

  • let declarations
  • subject declarations
  • Any mocking, stubbing or test double declaration
  • 任何嘲弄,存根或測試雙重聲明

other frameworks

Mock 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.

模擬對象框架和數據庫事務管理器(如ActiveRecord)通常是圍繞在示例之前設置,運行該示例,然后拆除的想法而設計的。這意味着模擬和存根可以(有時)在before(:context)之前聲明,但是在第一個真實示例運行之前就會被拆除。

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.

您可以在rspec-rails中的before(:context)中創建數據庫支持的模型對象,但它不會包含在事務中,因此您可以自行清理after(:context)塊。

(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.

只要您了解您的before(:context)掛鈎超出了測試雙打和數據庫事務等正常的每個示例生命周期,並且管理必要的設置並明確地自行拆解,那么你會很好 - 但是其他人你的代碼庫上的工作可能不知道這些陷阱。

#2


1  

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.

在版本3.3中添加的aggregate_failures將執行您要求的一些內容。它允許您在規范內部有多個期望,RSpec將運行每個期望並報告所有故障,而不是在第一個故障停止。

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

#3


0  

@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):

@Myron Marston提供了一些靈感,所以我第一次嘗試以或多或少可重用的方式實現它最終得到以下用法(請注意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.

當然,shared_subject假定共享狀態及其所有怪癖。

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.

更重要的是,我們需要做的就是處理狀態泄漏(應該是那些潛入的)是將shared_subject替換回主題。然后你運行正常的RSpec示例。

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

我確信實施有一些怪癖,但應該是一個非常好的開始。


注意!

本站翻译的文章,版权归属于本站,未经许可禁止转摘,转摘请注明本文地址:https://www.itdaan.com/blog/2014/09/03/72530845f0be26bf52706f33c9665501.html



 
粤ICP备14056181号  © 2014-2021 ITdaan.com