如何在rspec中测试方法调用的顺序?

时间:2015-08-01 13:24:14

标签: ruby testing rspec

我有一个使用命令模式的类按顺序执行一系列简单的转换步骤。数据作为数据馈送(以XML格式)传入,然后使用单一用途的步骤类通过多个步骤进行转换。所以它可能看起来像这样(实际的类名不同):

raw_data = Downloader.new(feed)
parsed_data = Parser.new(raw_data)
translated_data = Translator.new(parsed_data)
sifted_data = Sifter.new(translated_data)
collate_data = Collator.new(sifted_data)

我为每个类都进行了单元测试,并且我有集成测试来验证完整的流程,包括调用每个类。

但我没有办法测试他们被称为的顺序

我喜欢一些测试,所以我知道:首先调用Downloader,然后是Parser,然后是Translator等。

这是Ruby和Rspec 3。

我确实找到了这个:http://testpractices.blogspot.com/2008/07/ordered-method-testing-with-rspec.html但是这是从2008年开始的,它也非常难看。有没有更好的方法来测试方法执行顺序?

谢谢!

3 个答案:

答案 0 :(得分:13)

RSpec Mocks至少提供ordered,因为至少RSpec 3.0:

  

您可以使用ordered来约束多个邮件预期的顺序。这通常不建议使用,因为在大多数情况下,顺序无关紧要,使用有序会使您的规格变得脆弱,但它偶尔会有用。当您使用ordered时,只有在按照声明的顺序接收消息时,该示例才会通过。

请注意,RSpec同意@spickermann认为这不是推荐的做法。但是,在某些情况下有必要,特别是在处理遗留代码时。

这是RSpec传递的例子:

RSpec.describe "Constraining order" do
  it "passes when the messages are received in declared order" do
    collaborator_1 = double("Collaborator 1")
    collaborator_2 = double("Collaborator 2")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_2).to receive(:step_2).ordered
    expect(collaborator_1).to receive(:step_3).ordered

    collaborator_1.step_1
    collaborator_2.step_2
    collaborator_1.step_3
  end
end

失败的例子:

RSpec.describe "Constraining order" do
  it "fails when messages are received out of order on one collaborator" do
    collaborator_1 = double("Collaborator 1")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_1).to receive(:step_2).ordered

    collaborator_1.step_2
    collaborator_1.step_1
  end

  it "fails when messages are received out of order between collaborators" do
    collaborator_1 = double("Collaborator 1")
    collaborator_2 = double("Collaborator 2")

    expect(collaborator_1).to receive(:step_1).ordered
    expect(collaborator_2).to receive(:step_2).ordered

    collaborator_2.step_2
    collaborator_1.step_1
  end
end

答案 1 :(得分:2)

我认为方法调用的顺序并不重要,不应该进行测试。重要的是方法的结果,而不是其内部结构。测试内部方法调用的顺序(而不仅仅是测试方法的结果)将使以后重构方法变得更加困难。

但是如果仍然想测试顺序,那么你可能想要测试方法是用前面调用的方法的模拟结果调用的:

let(:raw_data)    { double(:raw_data) }
let(:parsed_data) { double(:parsed_data) }
# ...

before do
  allow(Downloader).to_receive(:new).and_return(raw_data)
  allow(Parser).to_receive(:new).and_return(parsed_data)
  # ...
end

it 'calls method in the right order' do
  foo.bar # the method you want to test

  expect(Downloader).to have_received(:new).with(feed)
  expect(Parser).to have_received(:new).with(raw_data)
  # ...
end

答案 2 :(得分:0)

我还要指出,一般来说,您不应该测试执行顺序,但是存在合理的应用情况。考虑一下涉及并发性的测试代码,例如Rails中的以下后台作业:

class MyJob < ActiveJob::Base
  def perform
    some_data.each do |item|
      create(item, parent: parent)
      item.do_something
      item.processed = true
      CleanUpJob.perform_later(item)
    end
  end
end

class CleanUpJob < ActiveJob::Base
  def perform
    # this can return true without waiting for each item to be created
    return unless item.parent.items.all?(&:processed?)
    # this also removes the parent!
    item.destroy
  end
end

在这里,您应该确保在创建每个项目之后改为执行CleanUpJob,就像这样:

class MyJob < ActiveJob::Base
  def perform
    items = []
    some_data.each do |item|
      create(item, parent: parent)
      item.do_something
      item.processed = true
      items.push(item)
    end

    items.each { |item| CleanUpJob.perform_later(item) }
  end
end

对于不支持ordered的框架(或较旧的版本),您可以做的是停止在稍后需要执行的调用上执行,并验证是否发生了先前的所有调用:

it "creates all items before scheduling cleanup" do
  expect(CleanUpJob).to receive(:perform_later).and_rasie("let's stop here")

  expect do
    MyJob.perform
  end.to raise_error(/let's stop here/)

  expect(items.count).to eq(some_data.count)
end

同样,这是为特殊情况而设计,并非临时使用。