如何在不使用rspec将测试与测试代码耦合的情况下进行存根/模拟?

时间:2017-09-26 08:19:26

标签: ruby-on-rails unit-testing rspec rspec-mocks

说那是一个工人,他的工作是:

  • 通过一组评论来查找或创建记录;
  • 更新记录的属性。

以下是一个示例实现:

class HardWorker
  include SidekiqWorker

  def perform(foo_id, bar_id)
    record = find_or_create(foo_id, bar_id)

    update_record(record)
  end

  private

  def find_or_create(foo_id, bar_id)
    MyRecord.find_or_create_by(foo_id: foo_id, bar_id: bar_id)
  end

  def update_record(record)
    result_of_complicated_calculations = ComplicatedService.new(record).call

    record.update(attribute: result_of_complicated_calculations)
  end
end

我想测试一下:

  • 如果记录不存在,工人会创建记录;
  • 工作人员没有创建新记录,但如果记录存在则提取现有记录;
  • 在任何情况下,工作人员都会更新记录

测试最后一种方法的一种方法是使用expect_any_instance_of

expect_any_instance_of(MyRecord).to receive(:update)

问题是expect/allow_any_instance_of的使用是discouraged

  

rspec-mocks API是为单个对象实例设计的,但此功能适用于整个对象类。结果,存在一些语义上令人困惑的边缘情况。例如,在expect_any_instance_of(Widget).to receive(:name).twice中,它不清楚每个特定实例是否应该接收两次名称,或者是否预期两个接收总数。 (它是前者。)

     

使用此功能通常是一种设计气味。可能是您的测试试图做得太多或者被测对象太复杂了。

     

这是rspec-mocks最复杂的功能,并且历史上收到的bug报告最多。 (没有一个核心团队积极使用它,这没有帮助。)

正确的方法是使用instance_double。所以我会尝试:

record = instance_double('record')

expect(MyRecord).to receive(:find_or_create_by).and_return(record)

expect(record).to receive(:update!)

这很好,但是,如果我有这个实现,那该怎么办:

MyRecord.includes(:foo, :bar).find_or_create_by(foo_id: foo_id, bar_id: bar_id)

现在,expect(MyRecord).to receive(:find_or_create_by).and_return(record),不会工作,因为 实际上,接收find_or_create_by的对象是一个实例 MyRecord::ActiveRecord_Relation

所以现在我需要将调用存根到includes

record = instance_double('record')

relation = instance_double('acitve_record_relation')

expect(MyRecord).to receive(:includes).and_return(relation)

expect(relation).to receive(:find_or_create_by).and_return(record)

另外,我可以这样称呼我的服务:

ComplicatedService.new(record.baz, record.dam).call

现在,我发现baz收到意外消息damrecord的错误。 现在我需要expect/allow这些消息或使用 Null object double

所以在这之后,我最终得到了一个测试,它非常紧密地反映了实现 正在测试的方法/类。我为什么要关心,还有一些额外的 在获取记录时,记录是通过includes急切加载的吗?我为什么要在乎, 在致电update之前,我还会在记录上调用一些方法(bazdam)?

这是rspec-mocks框架的限制/框架的哲学还是我的 我用错了吗?

1 个答案:

答案 0 :(得分:3)

我稍微修改了初始版本以便更容易测试:

class HardWorker
  include SidekiqWorker

  def perform(foo_id, bar_id)
    record = find_or_create(foo_id, bar_id)

    update_record(record)
  end

  private

  def find_or_create(foo_id, bar_id)
    MyRecord.find_or_create_by(foo_id: foo_id, bar_id: bar_id)
  end

  def update_record(record)
    # change for easier stubbing
    result_of_complicated_calculations = ComplicatedService.call(record)

    record.update(attribute: result_of_complicated_calculations)
  end
end

方式,我会测试这个:

describe HardWorker do
  before do
    # stub once and return an "unique value"
    allow(ComplicatedService).to receive(:call).with(instance_of(HardWorker)).and_return :result_from_service
  end

  # then do two simple tests
  it 'creates new record when one does not exists' do
    allow(ComplicatedService).to receive(:call).with(instance_of(HardWorker)).and_return :result_from_service
    HardWorker.call(1, 2)

    record = MyRecord.find(foo_id: 1, bar_id: 2)

    expect(record.attribute).to eq :result_from_service
  end

  it 'updates existing record when one exists' do
    record = create foo_id: 1, bar_id: 2

    HardWorker.call(record.foo_id, record.bar_id)

    record.reload

    expect(record.attribute).to eq :result_from_service
  end
end