簡體   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