[英]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
我想測試一下:
測試最后一種方法的一種方法是使用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
收到意外消息baz
和dam
。 現在我需要expect/allow
這些消息或使用Null對象double 。
所以在這之后,我最終得到了一個測試,它超級緊密地反映了正在測試的方法/類的實現。 我為什么要關心,在獲取記錄的同時,通過includes
一些額外的記錄是急切加載的? 我為什么要關心,在調用update
之前,我還會在記錄中調用一些方法( baz
, dam
)?
這是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.