对象构造函数创建另一个对象时的存根化问题

6

我有一些代码,非常简化后,看起来像这样:

class B
  def initialize opts
    @opts = opts
  end
end

class A
  def initialize opts
    # defaults etc applied to opts
    @b = B.new opts
  end
end

换句话说,在我使用选项初始化A时,它会创建一个B并将修改后的选项传递给它。
我想测试B.new是否得到了正确的参数。目前,我是这样做的,使用RSpec/RR:
@b = Object.new
# stub methods on @b here
stub(B).new { |options|
  options[:foo].should == 'whatever'
  @b
}
A.new({:foo => 'whatever'})

但是这有两个问题。
首先,我无法使用实际选项来实例化 B 的实际副本。如果我在块内调用 B.new,它会调用存根版本并循环直到堆栈弹出。我可以在存根之前设置 @b = B.new,但我不知道将传递哪些选项,这使测试失去了意义。
(在某人指责我的时候:是的,在严格的单元测试教条中,对 A 的测试应该存根掉 B 中的任何方法,并且需要存根很多意味着你的代码本身就是有问题的。)
其次,把 should 放在测试的设置中,而不是在单独的 it ... do ... end 块中,感觉就是错误的。但是由于我无法创建一个实际的 B(参见上文),因此我也无法真正询问它的后构建状态。
有什么想法吗?
3个回答

10

根据Marc-André Lafortune的回答,在RSpec 3中should语法已经过时。然而以下的expect语法似乎可以使用:

expect(B).to receive(:new).with(foo: 'whatever')

请注意,如果您想让B.new返回特定实例(例如测试替身),您可以使用and_return

b = instance_double(B)
expect(B).to receive(:new).with(foo: 'whatever').and_return(b)

3
你可以写类似于B.should_receive(:new).with({:foo => 'whatever'})的代码。
个人而言,我避免使用模拟和桩测试,而更喜欢测试行为;创建一个具有给定选项的新B是实现相关的,我不会直接测试这一点。

糟糕!谢谢,我简直不敢相信我之前没看到这种做法。将RR版本作为单独的答案添加。 - lambshaanxy

1

马克-安德烈的答案的RR版本:

before do
  stub(B).new { @b }
end

it 'uses the correct options' do
  B.should have_received.new(hash_including(:foo => 'whatever'))
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接