【问题标题】:How to properly stub doubles如何正确存根双打
【发布时间】:2015-03-23 17:54:32
【问题描述】:

正在测试的代码:

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 将其指定为 :putsout。我尝试将其与allow(Interface).to receive(:new).with(session).and_return(interface) 绑定,但这并没有改变。如何让经过测试的Session 类看到双精度/实例双精度并通过测试?

【问题讨论】:

  • 问题是@out.puts "hello".. 写@out.send :puts, "hello".. 为什么?阅读你的例外,它在告诉你。 .:D
  • 你能解释一下.send吗?这对我来说是新的,在例外情况中并不明显。
  • 这会破坏fake_stdout,使其打印到控制台并弄乱测试套件的输出。 fake_stdout 似乎没有正确绑定。

标签: ruby rspec mocking stubbing rspec3


【解决方案1】:

我认为,这不是存根的真正问题,而是一般方法。在为某个类编写单元测试时,您应该坚持该类的功能,并最终坚持它所看到的 API。如果您正在使用“内部”outInterface - 对于 Session 的规格来说已经太多了。

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

Ufff...这是很长的介绍/解释,但是如何进行呢? (也称为给我看代码!;))。

话虽如此,Session.new 应该只知道Interfaces hello 方法,它应该相信它可以正常工作,并且Sessions 规范应该确保调用该方法。为此,我们将使用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

test spy 是一个函数,它记录参数、返回值、this 的值以及所有调用所引发的异常(如果有)。

这来自SinonJS(这是谷歌搜索“什么是测试间谍”时的第一个结果),但解释是准确的。

这是如何工作的?

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

首先,我们正在执行一些代码,然后我们断言预期的事情已经发生。从概念上讲,我们希望确定,在Session.new 期间,fake_interface have_received(:hello)。就是这样!

好的,但我需要另一个测试来确保 Interfaces 方法是使用 特定 参数调用的

好的,让我们测试一下!

假设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

这个很简单。

还有一件事——我的InterfaceSession 作为参数。如何测试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 只知道调用了正确的方法,但里面发生了什么?这就是Interfaces 的工作...

希望对您有所帮助!请在我的other answer 中查看spy 的另一个示例。

祝你好运!

【讨论】:

  • 虽然间谍似乎是一种风格选择(根据我对 RSpec 文档的理解),但这是我在 RSpec 3.X 白话中看到的第一个也是最好的存根解释。您是对的,我的方法缺乏(为此进行了许多更改),但是如果您将其写成一些独立的文章,仅在这种级别的上下文、细节、和解释。一个多月以来,我一直在试图弄清存根的意义,这就是让它点击的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-03-18
  • 1970-01-01
  • 2015-02-18
  • 1970-01-01
  • 1970-01-01
  • 2013-10-24
  • 1970-01-01
相关资源
最近更新 更多