Imagine you needed to write a class, Foo
, which needed to access a remote service, which you did through a class known as CrazyInterface
. You might, as a first shot, write the constructor for your class like this:
class Foo
def initialize(interface_address, interface_port)
@madness = CrazyInterface.new(interface_address, interface_port)
end
end
The problem is that this is really hard to test. In order to get a mock/stub object into @madness
, you either need to reach in and swap out the real one for the mock:
test_obj = Foo.new('localhost', 12345)
test_obj.instance_variable_set(:@madness, ci_mock = double(CrazyInterface))
expect(ci_mock).to receive(:something)
Or use RSpec's any_instance_of
matcher:
expect_any_instance_of(CrazyInterface).to receive(:something)
test_obj = Foo.new('localhost', 12345)
test_obj.frob
Or you can stub the CrazyInterface.new
to return your mock:
allow(CrazyInterface).to receive(:new).and_return(ci_mock = double(CrazyInterface))
expect(ci_mock).to receive(:something)
All of these are hideous. Worse, if you ever need to pass more parameters into CrazyInterface.new
, you'll need to change the signature of Foo.new
to match. Not cool on so many levels.
Instead, you should change Foo
's constructor so that it takes an already initialized instance of CrazyInterface
, like this:
class Foo
def initialize(crazy_interface)
@madness = crazy_interface
end
end
This makes testing easy:
ci_mock = double(CrazyInterface)
foo = Foo.new(ci_mock)
expect(ci_mock).to receive(:something)
foo.frob