1

假设有一个工人,他的工作是:

  • 通过一组标准查找或创建记录;
  • 更新记录的属性。

这是一个示例实现:

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问题是不鼓励使用 of :

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

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

它是 rspec-mocks 中最复杂的功能,历史上收到的错误报告最多。(核心团队都没有积极使用它,这无济于事。)

正确的方法是使用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_byMyRecord::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并由. 现在我需要这些消息或使用 Null 对象 doubledamrecordexpect/allow

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

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

4

1 回答 1

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
于 2017-09-26T08:50:16.567 回答