繁体   English   中英

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

[英]How to stub/mock without coupling the test to the code under test with rspec?

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

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

这是一个示例实现:

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

我想测试一下:

  • 如果记录不存在,工人会创建记录;
  • worker不创建新记录,但如果记录存在则提取现有记录;
  • 无论如何,工作人员更新记录

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

expect_any_instance_of(MyRecord).to receive(:update)

问题是不鼓励使用expect/allow_any_instance_of

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

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

这是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_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

现在,我会得到错误, record收到意外消息bazdam 现在我需要expect/allow这些消息或使用Null对象double

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

这是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)
    # 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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM