Categories
rspec Ruby Software

Interesting little rSpec gotcha

This one had Adam and me stumped for a while. Trying to check that a method is only called on objects that respond to it:

describe "Foo#call_all_the_things" do
  let(:foo_1) { stub :foo_1, bar: "hello" }
  let(:foo_2) { stub :foo_2 }
  subject { Foo.new foo_1, foo_2 }

  it "only calls bar on objects that respond to it" do
    foo_1.should_receive :bar
    foo_2.should_not_receive :bar
    subject.call_all_the_things(:bar)
  end
end

class Foo
  def initialize *things
    @things = things
  end

  def call_all_the_things method
    @things.each do |thing|
      thing.send method if thing.respond_to? method
    end
  end
end


  1) Foo#call_all_the_things only calls bar on objects that respond to it
     Failure/Error: thing.send method if thing.respond_to? method
       (Stub :foo_2).bar(no args)
           expected: 0 times
           received: 1 time

Hmm. Why is it calling bar on the thing that doesn’t respond to it? Perhaps rSpec doubles don’t handle respond_to? properly?

[1] pry(main)> require "rspec/mocks/standalone"
=> true
[2] pry(main)> foo = stub foo: 123
=> #
[3] pry(main)> foo.respond_to? :foo
=> true
[4] pry(main)> foo.respond_to? :bar
=> false

Nope.

FX: lightbulb above head

Of course! To do the should_not_receive check, it needs to stub the method, which means it responds to it!

Two possible solutions: either let the fact that the missing method isn’t called be tested implicitly, or specify that when objects that don’t respond to the method exist, no NoMethodError is raised.

Categories
rspec

A couple of rspec mocking gotchas

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:

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

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?

describe 'Calling a method that catches StandardError' do
  it 'does NOT call Bar.bar' do
    Bar.should_not_receive :bar
    Foo.foo
  end
end
$ 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:

class Foo
  def self.foo
    Bar.bar
  end
end
$ 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:

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
$ 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 expected :bar with (any args) once, but received it twice error that you’d expect. Unfortunately the second call to the method is handled by the stub, so never triggers the error.

You can fix it, but it’s messy:

it 'only calls a method once' do
  Bar.send(:__mock_proxy).reset
  Bar.should_receive(:bar).once
  Foo.foo
end
$ 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.

Categories
rspec

Helpful message from rspec

Just came across an interesting error message from rspec. I had a spec that looked like this:

it "should not mass-assign 'confirmed'" do
  Blog.new(:confirmed => true).confirmed.should_not be_true
end

Obviously it failed, as I hadn’t written the code yet, but there was more in the error message than I expected:

..........F

1)
RuntimeError in 'Blog should not mass-assign 'confirmed''
'should_not be  true' not only FAILED,
it is a bit confusing.
It might be more clearly expressed in the positive?
.../spec/models/blog_spec.rb:20:

Finished in 0.06192 seconds

11 examples, 1 failure

In fact, rewriting this as should be_false wouldn’t work, as the expected value is nil. I took the hint though, and rewrote it as should be_nil.

Categories
Rails rspec Ruby

“You have to declare the controller name in controller specs”

For ages I’ve been getting an intermittent problem with RSpec, where occasionally I’d see the following error on a model spec:

You have to declare the controller name in controller specs. For example:
describe "The ExampleController" do
controller_name "example" #invokes the ExampleController
end

The problem seemed to depend on which order the specs were run in, and for rake it could be avoided by removing --loadby mtime --reverse from spec.opts. It was a real pain with autotest though, and today (my original plan of “wait for RSpec 1.1 and hope it goes away” having failed) I finally got round to looking into it properly.

It seemed that the error was being triggered by the rather unpleasant code I wrote a while ago to simplify testing of model validation. Digging into the RSpec source to see what was happening, I found that that error message only gets returned when (as you’d expect) you don’t declare the controller name in a controller spec (specifically in an instance of Spec::Rails::Example::ControllerExampleGroup). The code that decides what type of example group to create lives in Spec::DSL::BehaviourFactory, and according to its specs, there are two methods it uses to figure out what type of spec it’s looking at:

it "should return a ModelExampleGroup when given :type => :model" do
...
it "should return a ModelExampleGroup when given :spec_path => '/blah/spec/models/'" do
...
it "should return a ModelExampleGroup when given :spec_path => '\\blah\\spec\\models\\' (windows format)" do
...
it "should favor the :type over the :spec_path" do
...

I began to suspect that the problem was caused by the fact that my specify_attributes method wasn’t declared in a file in spec/models, so I thought I’d try specifying the type explicitly. So instead of this:

describe "#{label} with all attributes set" do

I changed it to this:

describe "#{label} with all attributes set", :type => 'model' do

Sure enough, it worked! Not sure whether anyone else is likely to see the same problem (unless they’re foolish enough to use my validation spec code), but hopefully if you do, a Google search will bring up this post and it might point you in the right direction.