简体   繁体   English

如何测试方法被调用

[英]How to test that a method is called

I have the following: 我有以下内容:

class Foo
  def bar(some_arg)
  end
end

It is called as Foo.new.bar(some_arg) . 它称为Foo.new.bar(some_arg) How do I test this in rspec? 如何在rspec中对此进行测试? I don't know how to know whether I've created an instance of Foo that has called bar . 我不知道如何知道是否创建了名为barFoo实例。

receive_message_chain is considered a smell as it makes it easy to violate the Law of Demeter . receive_message_chain被认为是一种气味,因为它很容易违反Demeter法则

expect_any_instance_of is considered a smell in that it is not specific as to which instance of Foo is being called. expect_any_instance_of被认为是一种气味,因为它不特定于正在调用哪个Foo实例。

As @GavinMiller noted, those practices are generally reserved for legacy code that you do not control. 正如@GavinMiller指出的那样,这些做法通常保留给您无法控制的旧代码。

Here's how to test Foo.new.bar(arg) without either: 这是不测试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

Note: I'm hard coding the arg here for simplicity. 注意:为简单起见,我在这里很难对arg进行编码。 But, you could just as easily mock it, too. 但是,您也可以轻松模拟它。 Showing that here would depend on how the arg is instantiated. 在这里显示取决于arg的实例化方式。

EDIT 编辑

It is important to note that these tests are intimately familiar with the underlying implementation. 重要的是要注意,这些测试对底层实现非常熟悉。 Therefore, if you change the implementation, the tests will fail. 因此,如果您更改实现,则测试将失败。 How to fix that issue depends on what exactly the Baz#do_something method does. 如何解决该问题取决于Baz#do_something方法的确切作用。

Let's say Baz#do_something actually just looks up a value from Foo#bar based on the arg and returns it without changing state anywhere. 假设Baz#do_something实际上只是基于argFoo#bar查找一个值,然后将其返回而不更改任何位置的状态。 (This is called a Query method.) In that case, our tests should not care about Foo at all, they should only care that the correct value is returned by Baz#do_something . (这称为Query方法。)在这种情况下,我们的测试根本不需要关心Foo,而应该只关心Baz#do_something返回的正确值。

On the other hand, let's say that Baz#do_something actually does change state somewhere, but does not return a testable value. 另一方面,假设Baz#do_something实际上确实在某处更改了状态,但未返回可测试的值。 (This is called a Command method.) In this case, we need to assert that the correct collaborators were called with the correct parameters. (这称为Command方法。)在这种情况下,我们需要断言使用正确的参数调用了正确的协作者。 But, we can trust that the unit tests for those other objects will actually test their internals, so we can use mocks as placeholders. 但是,我们可以放心,对其他对象的单元测试实际上将测试其内部,因此我们可以使用模拟作为占位符。 (The tests I showed above are of this variety.) (我在上面显示的测试是各种各样的。)

There's a fantastic talk on this by Sandi Metz from back in 2013. The specifics of the technologies she mentions have changed. 自2013年以来,Sandi Metz对此进行了精彩的演讲 。她提到的技术细节已经改变。 But, the core content of how to test what is 100% relevant today. 但是,如今如何测试100%相关的核心内容。

If you're mocking this methods in another class spec (say BazClass), then the mock method would just return an object with the information you are expecting. 如果要在另一个类规范(例如BazClass)中模拟此方法,则模拟方法将只返回一个具有所需信息的对象。 For example, if you use Foo#bar in this Baz#some_method spec, you can do this: 例如,如果在此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

otherwise if you want the Foo to actually call the method and run it, then you would just call the method regularly 否则,如果您希望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

edit: 编辑:

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

Easiest way is to use expect_any_instance_of . 最简单的方法是使用expect_any_instance_of

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

That said, this method is discouraged since it's complicated, it's a design smell, and it can result in weird behaviour. 就是说,不建议使用此方法,因为它很复杂,有设计气味,并且可能导致奇怪的行为。 The suggested usage is for legacy code that you don't have full control over. 建议的用法用于您无法完全控制的旧代码。


Speculating on what your code looks like, I'd expect something like this: 推测您的代码是什么样的,我期望这样的事情:

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

If this is the situation you find yourself in, you're best off to raise the class instantiation into a default argument like this: 如果您遇到这种情况,最好将类实例化为默认参数,如下所示:

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

That way you can pass in a mock in place of the default instantiation: 这样,您可以传递模拟来代替默认实例:

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

  # ...
end

This is known as dependency injection. 这称为依赖注入。 It's a bit of a forgotten art in Ruby but if you read up about Java testing patterns you'll find it. 在Ruby中这是一门被遗忘的艺术,但是如果您了解Java测试模式,就会发现它。 The rabbit hole goes pretty deep though once you start going that route and tends to be overkill for Ruby. 尽管一旦您开始走这条路,兔子洞就会变得很深,对Ruby来说可能会显得过大。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM