Reputation: 11173
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
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
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
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