【问题标题】:Is there a less intrusive alternative to Rspec's `should_receive`?有没有比 Rspec 的 `should_receive` 更少干扰的替代方法?
【发布时间】:2012-08-22 23:27:21
【问题描述】:

在编写 Rspec 测试时,我经常对 should_receive 感到沮丧。我想知道是否有不那么侵入性的替代方案。

例如:

describe "making a cake" do
  it "should use some other methods" do
    @baker.should_receive(:make_batter)
    @baker.make_cake
  end
end

should_receive 的调用是一个很好的描述,但它破坏了我的代码,因为should_receive 通过屏蔽原始方法起作用,并且make_cake 无法继续,除非make_batter 实际上返回一些击球手。所以我把它改成这样:

@baker.should_receive(:make_batter).and_return(@batter)

这很丑,因为:

  • 看起来好像我正在测试 make_batter 正确返回 @batter,但我实际上强制 make_batter 的假版本返回它.
  • 强制我单独设置@batter
  • 如果make_batter 有任何重要的副作用(我想可能是代码异味),我也必须让这些发生。

我希望should_receive(:make_batter) 能够验证方法调用并将其传递给原始方法。如果我想对它的行为进行存根以进行更好的隔离测试,我会明确地这样做:@baker.stub(:make_batter).and_return(@batter)

有没有办法在不阻止原始方法调用的情况下执行should_receive 之类的操作?我的问题是糟糕设计的征兆吗?

【问题讨论】:

    标签: ruby rspec


    【解决方案1】:

    您在使用 should_receive 时遇到此问题,因为您正在测试 make_cake 方法的实现细节。在编写测试时,您应该只关注行为而不是一系列内部方法调用。否则,稍后重构您的代码也会导致对所有测试的大量重构。

    当您想单独测试您的类时,Mocks 和 Stub 会派上用场。在编写单元测试时,您的测试对象应该与任何其他对象隔离。当您同时处理多个对象时,两者都充当替身。在这种情况下,您可以使用should_receive 来确保您的测试对象通过调用方法正确地将某些任务委托给另一个对象。

    【讨论】:

    • 您关于不测试实现细节的总体观点是正确的,但似乎这是should_receive 的唯一用例,对吧?如果foo 应该返回一些东西,我仍然不清楚如何在没有and_return 的情况下使用should_receive(:foo)
    • 另外,我的糟糕例子可能掩盖了这个问题。假设我为某个库编写了一个包装方法。我想测试我的包装器是否使用正确的参数调用底层库方法,并且我的方法还需要在返回之前解析它返回的内容。我想这让我回到some_library.should_receive(:some_method).with('foo', 'bar').and_return('thingy'),对吧?在这种情况下,我不是在测试实现细节,而是在测试包装方法的目的。
    • 在这种情况下,您当然想要返回一些东西。但是为了保持隔离,我会伪造库的返回值。 (一个很好的例子是一个包装 Web 服务的库。我不希望我的测试在每次运行它们时都调用 Web 服务,我不想依赖所有可能出错的部分调用它)这当然是我只会在单元测试中做的事情。在编写集成测试时,我希望触发所有级别的实现。
    【解决方案2】:

    您可以让should_receive 像这样运行原始方法:

    @baker.should_receive(:make_batter, &@baker.method(:make_batter))
    

    should_receivestub 都支持传递块实现(即在调用方法时进行评估)。 &@baker.method(:make_batter) 获取原始方法对象的句柄,并将其作为块实现传递。

    FWIW,我们希望提供一个更好的 API 来委托给原始方法(请参阅 this issue),但很难在不破坏向后兼容性的情况下添加该功能。

    【讨论】:

    • 整洁!我没有意识到should_receive 可以阻止。也很高兴看到我不是唯一一个想要这个的人。
    • 替代(可能不可行)想法:在我的示例中,如果在收到make_batter 后,测试只是从bake_cake 中退出怎么办?无论如何,我不应该一次测试超过一件事,对吧?
    • 我为此创建了一个小辅助方法:def should_receive_and_proceed(object, method); object.should_receive(method, &object.method(method)); end
    • 鉴于现在是 2016 年,我认为上面@Tyler Rick 的回答更合适:stackoverflow.com/a/14693779/224707
    【解决方案3】:

    看起来更好的 API 可以委托给 Myron Marston 提到的原始方法实际上已添加到 rspec-mocks v2.12.0

    因此,现在您可以在“想要设置消息预期而不干扰对象对消息的响应方式”的任何时候简单地执行此操作:

    @baker.should_receive(:make_batter).and_call_original
    

    感谢您添加此内容,迈伦。

    【讨论】:

    • 感谢以更新的答案重新审视这个问题。
    猜你喜欢
    • 1970-01-01
    • 2017-07-19
    • 2018-12-05
    • 1970-01-01
    • 1970-01-01
    • 2012-06-30
    • 2013-06-22
    • 2018-01-12
    • 1970-01-01
    相关资源
    最近更新 更多