Just a couple of things that have caused a bit of head-scratching lately when writing RSpec specs using the built-in mocking framework.
Catching StandardError
Watch out if the code you’re testing catches StandardError
(of course you’re not catching Exception
, right?). Try this:
[ruby]
require ‘rubygems’
require ‘spec’
class Foo
def self.foo
Bar.bar
rescue StandardError
# do something here and don’t re-raise
end
end
class Bar
def self.bar
end
end
describe ‘Calling a method that catches StandardError’ do
it ‘calls Bar.bar’ do
Bar.should_receive :bar
Foo.foo
end
end
[/ruby]
Nothing particularly exciting there. Let’s run it and check that it passes:
$ spec foo.rb . Finished in 0.001862 seconds 1 example, 0 failures
However, what if we change the example to test the opposite behaviour?
[ruby]
describe ‘Calling a method that catches StandardError’ do
it ‘does NOT call Bar.bar’ do
Bar.should_not_receive :bar
Foo.foo
end
end
[/ruby]
$ spec foo.rb . Finished in 0.001865 seconds 1 example, 0 failures
Wait, surely they can’t both pass? Let’s take out the rescue and see what’s going on:
[ruby]
class Foo
def self.foo
Bar.bar
end
end
[/ruby]
$ spec foo.rb F 1) Spec::Mocks::MockExpectationError in 'Calling a method that catches StandardError does NOT call Bar.bar'expected :bar with (no args) 0 times, but received it once ./foo.rb:6:in `foo' ./foo.rb:18: Finished in 0.002276 seconds 1 example, 1 failure
That’s more like it.
Of course, what’s really happening here is that Spec::Mocks::MockExpectationError
is a subclass of StandardError
, so is being caught and silently discarded by our method under test.
If you’re doing TDD properly, this won’t result in a useless test (at least not immediately), but it might cause you to spend a while trying to figure out how to get a failing test before you add the call to Foo.foo
(assuming the method with the rescue
already existed). Generally you can solve the problem by making the code a bit more selective about which exception class(es) it catches, but I wonder whether RSpec exceptions are special cases which ought to directly extend Exception
.
Checking receive counts on previously-stubbed methods
It’s quite common to stub a method on a collaborator in a before
block, then check the details of the call to the method in a specific example. This doesn’t work quite as you would expect if for some reason you want to check that the method is only called a specific number of times:
[ruby]
require ‘rubygems’
require ‘spec’
class Foo
def self.foo
Bar.bar
Bar.bar
end
end
class Bar
def self.bar
end
end
describe ‘Checking call counts for a stubbed method’ do
before do
Bar.stub! :bar
end
it ‘only calls a method once’ do
Bar.should_receive(:bar).once
Foo.foo
end
end
[/ruby]
$ spec foo.rb . Finished in 0.001867 seconds 1 example, 0 failures
I think what’s happening here is that the mock object would normally receive an unexpected call, causing the
You can fix it, but it’s messy:
[ruby]
it ‘only calls a method once’ do
Bar.send(:__mock_proxy).reset
Bar.should_receive(:bar).once
Foo.foo
end
[/ruby]
$ spec foo.rb F 1) Spec::Mocks::MockExpectationError in 'Checking call counts for a stubbed method only calls a method once'expected :bar with (any args) once, but received it twice ./foo.rb:23: Finished in 0.002542 seconds 1 example, 1 failure
Does anyone know a better way?
The full example code is in this gist.