2017-09-26 83 views
1

说有一个工人,他们的工作是:如何使用rspec将测试耦合到测试下的代码时进行存根/模拟?

  • 寻找或创造一组critaria的记录;
  • 更新记录的属性。

这里是一个示例实现:

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接收(:名称).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_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 

现在,我会得到的错误,意外消息bazdamrecord收到。 现在我需要expect/allow这些消息或使用 Null object double。因此,毕竟,我最终得到了一个测试,它严格反映了正在测试的方法/类的实现 。为什么我应该关心一些额外的 记录是通过includes热切加载的,同时获取记录?为什么我应该在意, 在致电update之前,我还会在记录上调用一些方法(baz,dam)?

这是rspec-mocks框架的限制/框架的理念还是我使用它错了?

回答

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