Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing when an object's constructor builds another object

So I've got some code that, grossly simplified, looks like this:

class B
  def initialize opts
    @opts = opts
  end
end

class A
  def initialize opts
    # defaults etc applied to opts
    @b = B.new opts
  end
end

In other words, when I initialize A with options, it creates a B and passes a modified set of options to it.

I would like to test that B.new gets the correct arguments. Right now, I'm doing it like this, using RSpec/RR:

@b = Object.new
# stub methods on @b here
stub(B).new { |options|
  options[:foo].should == 'whatever'
  @b
}
A.new({:foo => 'whatever'})

But this has two problems.

First, I can't instantiate an actual copy of B with the actual options. If I call B.new inside the block, it calls the stubbed version and loops until the stack pops. I can set @b = B.new before the stubbing, but I don't know the options that will be passed in yet, defeating the point of the test.

(And before somebody calls me out on it: yes, in strict unit test dogma, a test of A should stub out any methods in B, and needing to stub out a lot means your code is bad in the first place.)

Second, it just feels wrong to put the should in the setup of the test, instead of in a separate it ... do ... end block afterwards. But since I can't create an actual B (see above), I can't really interrogate its post-construction state either.

Any ideas?

like image 909
lambshaanxy Avatar asked Jul 06 '12 05:07

lambshaanxy


3 Answers

The should syntax from Marc-André Lafortune's answer appears to be deprecated in RSpec 3. The following expect syntax seems to work, however:

expect(B).to receive(:new).with(foo: 'whatever')

Note that if you want B.new to return a specific instance (e.g. a test double), you can use and_return:

b = instance_double(B)
expect(B).to receive(:new).with(foo: 'whatever').and_return(b)
like image 137
David Moles Avatar answered Oct 14 '22 09:10

David Moles


You could write something like B.should_receive(:new).with({:foo => 'whatever'}).

Personally, I avoid stubbing/mocking and would rather test for behavior; the fact that a new B is created with a given set of options is implementation dependent and I wouldn't test that directly.

like image 26
Marc-André Lafortune Avatar answered Oct 14 '22 07:10

Marc-André Lafortune


RR version of Marc-Andre's answer:

before do
  stub(B).new { @b }
end

it 'uses the correct options' do
  B.should have_received.new(hash_including(:foo => 'whatever'))
end
like image 1
lambshaanxy Avatar answered Oct 14 '22 07:10

lambshaanxy