如何测试方法被调用

时间:2018-12-14 03:51:05

标签: ruby unit-testing rspec

我有以下内容:

class Foo
  def bar(some_arg)
  end
end

它称为Foo.new.bar(some_arg)。如何在rspec中对此进行测试?我不知道如何知道是否创建了名为Foo的{​​{1}}实例。

3 个答案:

答案 0 :(得分:4)

receive_message_chain被认为是一种气味,因为它很容易违反Law of Demeter

expect_any_instance_of被认为是一种气味,因为它不特定于正在调用哪个Foo实例。

正如@GavinMiller指出的那样,这些做法通常保留给您无法控制的旧代码。

以下是如何测试Foo.new.bar(arg)的方法:

class Baz
  def do_something
    Foo.new.bar('arg')
  end
end

describe Baz do
  subject(:baz) { described_class.new }

  describe '#do_something' do
    let(:foo) { instance_double(Foo, bar: true) }

    before do
      allow(Foo).to receive(:new).and_return(foo)

      baz.do_something
    end

    it 'instantiates a Foo' do
      expect(Foo).to have_received(:new).with(no_args)
    end

    it 'delegates to bar' do
      expect(foo).to have_received(:bar).with('arg')
    end
  end
end

注意:为简单起见,我在这里对arg进行硬编码。但是,您也可以轻松模拟它。在这里显示取决于arg的实例化方式。

编辑

重要的是要注意,这些测试对底层实现非常熟悉。因此,如果您更改实现,则测试将失败。如何解决该问题取决于Baz#do_something方法的确切作用。

比方说,Baz#do_something实际上只是基于Foo#bararg中查找一个值并返回它,而没有在任何地方更改状态。 (这称为Query方法。)在这种情况下,我们的测试根本不需要关心Foo,而应该只关心Baz#do_something返回的正确值。

另一方面,假设Baz#do_something实际上确实在某处更改了状态,但没有返回可测试的值。 (这称为Command方法。)在这种情况下,我们需要断言使用正确的参数调用了正确的协作者。但是,我们可以放心,对其他对象的单元测试实际上将测试其内部,因此我们可以使用模拟作为占位符。 (我上面显示的测试是各种各样的。)

桑迪·梅斯(Sandi Metz)早在2013年就对此进行了精彩的评论talk。她提到的技术细节已经改变。但是,今天如何测试100%相关性的核心内容。

答案 1 :(得分:0)

如果要在另一个类规范(例如BazClass)中模拟此方法,则模拟方法将只返回一个包含您期望的信息的对象。例如,如果在此Baz#some_method规范中使用Foo#bar,则可以执行以下操作:

# Baz#some_method
def some_method(some_arg)
  Foo.new.bar(some_arg)
end

#spec for Baz
it "baz#some_method" do
  allow(Foo).to receive_message_chain(:bar).and_return(some_object)
  expect(Baz.new.some_method(args)).to eq(something) 
end

否则,如果您希望Foo实际调用该方法并运行它,则只需定期调用该方法

#spec for Baz
it "baz#some_method" do
  result = Baz.new.some_method(args)
  @foo = Foo.new.bar(args)
  expect(result).to eq(@foo) 
end

编辑:

it "Foo to receive :bar" do
  expect(Foo.new).to receive(:bar)
  Baz.new.some_method(args)
end

答案 2 :(得分:0)

最简单的方法是使用expect_any_instance_of

expect_any_instance_of(Foo).to receive(:bar).with(expect_arg).and_return(expected_result)

也就是说,不建议使用此方法,因为它很复杂,有设计气味,并且可能导致奇怪的行为。建议的用法用于您无法完全控制的旧代码。


推测您的代码是什么样的,我希望这样:

class Baz
  def do_stuff
    Foo.new.bar(arg)
  end
end

it 'tests Baz but have to use expect_any_instance_of' do
  expect_any_instance_of(Foo).to receive(:bar).with(expect_arg).and_return(expected_result)
  Baz.do_stuff
  # ...
end

如果您遇到这种情况,最好将类实例化为默认参数,如下所示:

class Baz
  def do_stuff(foo_instance = Foo.new)
    foo_instance.bar(arg)
  end
end

这样,您可以传递模拟来代替默认实例:

it 'tests Baz properly now' do
  mock_foo = stub(Foo)
  Baz.do_stuff(mock_foo)

  # ...
end

这称为依赖注入。有点forgotten art in Ruby,但是如果您阅读有关Java测试模式的内容,则会发现它。尽管一旦您开始走这条路,兔子洞就会变得很深,对Ruby来说可能会显得过分杀伤。