假设有一个工人,他的工作是:
- 通过一组标准查找或创建记录;
- 更新记录的属性。
这是一个示例实现:
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_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
并由. 现在我需要这些消息或使用
Null 对象 double。dam
record
expect/allow
所以在这一切之后,我最终得到了一个测试,它超级紧密地反映了正在测试的方法/类的实现。我为什么要关心,includes
在获取记录时,一些额外的记录是通过 预先加载的?我为什么要关心,在调用之前update
,我还调用了记录上的一些方法(baz
,dam
)?
这是 rspec-mocks 框架的限制/框架的哲学还是我用错了?