说那是一个工人,他的工作是:
以下是一个示例实现:
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
收到意外消息dam
和record
的错误。
现在我需要expect/allow
这些消息或使用
Null object double
所以在这之后,我最终得到了一个测试,它非常紧密地反映了实现
正在测试的方法/类。我为什么要关心,还有一些额外的
在获取记录时,记录是通过includes
急切加载的吗?我为什么要在乎,
在致电update
之前,我还会在记录上调用一些方法(baz
,dam
)?
这是rspec-mocks框架的限制/框架的哲学还是我的 我用错了吗?
答案 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