RSpec 允许我 select 使用匹配器进行模拟吗?

Does RSpec allow me to select mocks with a matcher?

我打了很多电话给

Kernel.open(url).read

在一些遗留代码中,我试图在大规模重构之前模拟这些代码以进行特性测试。

我不喜欢

忽略参数的方式
allow_any_instance_of(Kernel).to(receive_message_chain(:open, :read)
    .and_return(return_value))

所以我将其替换为

def stub_kernel_open_read(args: '[arg1, arg2, k: v, etc]', returning:)
  allow_any_instance_of(Kernel).to(receive(:open).with(*args)).and_return(double.tap { |d|
    allow(d).to(receive(:read)).and_return(returning)
  })
end

但我发现我遇到了这些错误:

       "http://my-fake-server/whatever" received :open with unexpected arguments
     expected: ("http://my-fake-server/whatever", {:http_basic_authentication=>["asdf", "asdf"]})
          got: ({:http_basic_authentication=>["asdf", "asdf"]})
   Diff:
   @@ -1,3 +1,2 @@
   -["http://my-fake-server/whatever",
   - {:http_basic_authentication=>["asdf", "asdf"]}]
   +[{:http_basic_authentication=>["asdf", "asdf"]}]

    Please stub a default value first if message might be received with other args as well. 

所以我发现如果我将我的存根扩展到这个:

allow_any_instance_of(Kernel).to(receive(:open).with(*args)) { |instance|
  return double.tap { |d|
    allow(d).to(receive(:read)) {
      return returning
    }
  }
}

然后 instance 具有 URL 的值。就目前而言这很好,我可以列出允许的 URL 列表,但感觉很糟糕。

有没有类似的

allow_any_instance_of(Kernel).that(eq('http://whatever')).to(receive(:open))

还是我完全走错了路?

显然我可以使用全局搜索替换来包装 Kernel.open(url).read 代码,并正确模拟该全局代码,但我希望尽可能避免这种情况。

AFAIU 你的问题是正确的,你需要这样的东西来告诉模拟行为 "normally":

allow(Kernel).to receive(:open).with(url).and_return(stub)
allow(Kernel).to receive(:open).with(anything).and_call_original # I can't check now, but there's a chance this one should go first, but I doubt it

然后

allow(stub).to receive(:read).and_return('something')

如果你必须模拟 Kernel.open 更多的 URL,它会变得有点乱,但原理是一样的

allow(Kernel).to receive(:open).with(first_url).and_return(first_stub)
allow(Kernel).to receive(:open).with(second_url).and_return(second_stub)
allow(Kernel).to receive(:open).with(anything).and_call_original # I can't check now, but there's a chance this one should go first


allow(first_stub).to receive(:read).and_return('something')
allow(second_stub).to receive(:read).and_return('something else')

除非我完全错过了你问题的重点?

我在 RSpec 的模拟系统中找不到任何东西来应对 'Kernel.open(url)' 方法模拟具有 url 作为实例的事实,如果你使用 allow_any_instance_of(Kernel).to(receive(:open))。所以我做了一个小酒馆,然后意识到我不需要 'any_instance_of' 位:

def stub_kernel_open_read(url: 'fake://your_url_parameter_is_wrong', args: ['arg1', 'arg2', k: :v_etc], **rest)
  allow(Kernel).to(receive(:open).with(*([url] + args)).and_return(reading_double(rest)))
end

def reading_double(return_or_raise)
  double.tap do |d|
    if return_or_raise[:returning]
      allow(d).to(receive(:read).and_return(return_or_raise[:returning]))
    else
      allow(d).to(receive(:read).and_raise(return_or_raise[:raising]))
    end
  end
end

此代码源于试图删除使用 'any_instance' 的 receive_message_chain

哦,我 运行 陷入各种奇怪的恶作剧中,试图用 (*a, **b) 参数对任何方法进行存根,尽管这似乎适用于我尝试烹饪的玩具示例,所以我不知道发生了什么。正是这种神秘的废话让我反对 Ruby 和鸭子打字。