如何正确存根双打

How to properly stub doubles

正在测试的代码:

class Session
  def initialize
    @interface = Interface.new(self)
    @interface.hello
  end
end

class Interface
  def initialize(session, out = $STDOUT)
    @session = session
    @out = out
  end

  def hello
    @out.puts "hello"
  end
end

测试:

describe Session do
  let (:fake_stdout) {double("$STDOUT", :puts => true)}
  let (:interface) {instance_double("Interface", :out => "fake_stdout")}
  let (:session) { Session.new }

  describe "#new" do
    it "creates an instance of Session" do
      expect(session).to be_an_instance_of(Session)
    end
  end
end

这会引发 private method 'puts' called for nil:NilClass。它似乎没有看到 fake_stdout 及其指定的 :puts 作为 out。我尝试将其与 allow(Interface).to receive(:new).with(session).and_return(interface) 绑定,但没有任何改变。如何让测试的 Session class 看到 double/instance 双倍并通过测试?

我认为,这不是存根问题,而是一般方法。在为某些 class 编写单元测试时,您应该坚持 class 的功能,并最终坚持 API 它看到的功能。如果您正在存根 "internal" out of Interface - 对于 Session.

的规格来说已经太多了

Session 真正看到的是 Interfaces public hello 方法,因此 Session 规范,不应该知道它的内部实现(它是 @out.puts "hello")。您真正应该关注的唯一一件事是,调用了 hello 方法。另一方面,确保 put 被调用 hello 应该在 Interface.

的规范中描述

Ufff...那么长introduction/explanation,但是接下来要怎么进行呢? (也称为 给我看代码! ;))。

话虽如此,Session.new 应该只知道 Interfacehello 方法,它应该相信它可以正常工作,并且 Session 的规范应该确保该方法被调用。为此,我们将使用 spy。让我们亲自动手!

RSpec.describe Session do
  let(:fake_interface) { spy("interface") }
  let(:session)        { Session.new }

  before do
    allow(Interface).to receive(:new).and_return(fake_interface)
  end

  describe "#new" do
    it "creates an instance of Session" do
      expect(session).to be_an_instance_of(Session) # this works now!
    end

    it "calls Interface's hello method when initialized" do
      Session.new
      expect(fake_interface).to have_received(:hello)
    end
  end
end

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.

这取自 SinonJS(这是谷歌搜索 "what is test spy" 时的第一个结果),但解释是准确的。

这是如何工作的?

Session.new
expect(fake_interface).to have_received(:hello)

首先,我们正在执行一些代码,然后我们断言预期事情发生了。从概念上讲,我们要确定 Session.newfake_interface have_received(:hello)。就这些!

好的,但我需要另一个测试确保 Interfaces 方法是用 specific 参数调用的 .

好的,让我们测试一下!

假设 Session 看起来像:

class Session
  def initialize
    @interface = Interface.new(self)
    @interface.hello
    @interface.say "Something More!"
  end
end

我们要测试say:

RSpec.describe Session do
  describe "#new" do
    # rest of the code

    it "calls interface's say_something_more with specific string" do
      Session.new
      expect(fake_interface).to have_received(:say).with("Something More!")
    end
  end
end

这个非常简单。

还有一件事 - 我的 Interface 使用 Session 作为参数。如何测试 interface 调用 sessions 方法?

让我们看一下示例实现:

class Interface
  # rest of the code

  def do_something_to_session
    @session.a_session_method
  end
end

class Session
  # ...

  def another_method
    @interface.do_something_to_session
  end

  def a_session_method
    # some fancy code here
  end
end

如果我说……就不足为奇了……

RSpec.describe Session do
  # rest of the code

  describe "#do_something_to_session" do
    it "calls the a_session_method" do
      Session.new.another_method
      expect(fake_interface).to have_received(:do_something_to_session)
    end
  end
end

您应该检查 Sessions another_method 是否调用了 interfaces do_something_to_session 方法。

如果你这样测试,你可以让测试不那么脆弱以应对未来的变化。您可以更改 Interface 的实现,使其不再依赖于 put。引入此类更改时 - 您只需更新 Interface 的测试。 Session 只知道调用了正确的方法,但是里面发生了什么?那是 Interface 的工作...

希望对您有所帮助!请看一下我的 .

中另一个 spy 的例子

祝你好运!