Terence Chow
Terence Chow

Reputation: 11173

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). How do I test this in rspec? I don't know how to know whether I've created an instance of Foo that has called bar.

Upvotes: 1

Views: 184

Answers (3)

aridlehoover
aridlehoover

Reputation: 3615

receive_message_chain is considered a smell as it makes it easy to violate the Law of Demeter.

expect_any_instance_of is considered a smell in that it is not specific as to which instance of Foo is being called.

As @GavinMiller noted, those practices are generally reserved for legacy code that you do not control.

Here's how to test Foo.new.bar(arg) without either:

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. But, you could just as easily mock it, too. Showing that here would depend on how the arg is instantiated.

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.

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. (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.

On the other hand, let's say that Baz#do_something actually does change state somewhere, but does not return a testable value. (This is called a Command method.) In this case, we need to assert that the correct collaborators were called with the correct parameters. 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. But, the core content of how to test what is 100% relevant today.

Upvotes: 4

Andy
Andy

Reputation: 165

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. For example, if you use Foo#bar in this Baz#some_method spec, you can do this:

# 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

#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

Upvotes: 0

Gavin Miller
Gavin Miller

Reputation: 43865

Easiest way is to use 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. The rabbit hole goes pretty deep though once you start going that route and tends to be overkill for Ruby.

Upvotes: 0

Related Questions