Skip to content

Instantly share code, notes, and snippets.

@mpalmer
Created May 26, 2015 12:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mpalmer/59c33fd1a2cef2181003 to your computer and use it in GitHub Desktop.
Save mpalmer/59c33fd1a2cef2181003 to your computer and use it in GitHub Desktop.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment