[rspec-users] long jumping out of code in specs

David Chelimsky dchelimsky at gmail.com
Mon Feb 5 07:53:05 EST 2007


On 2/5/07, Chris Anderson <jchris at mfdz.com> wrote:
> I wrote this abomination just now, and wonder if anyone else has had
> experience with the pattern behind it. In short, I'm specing an
> after_create hook on an ActiveRecord model, that calls a bunch of
> private methods. Instead of stubbing all those private methods (which
> is verboten anyway, as well as impossible because creating the object
> which owns those methods is what I'm specing, so I don't have the
> object until I call do_action.)
>
> I'm stubbing a public method that would only get called if
> control-flow worked out like it should (the proverbial canary in the
> coal mine), and telling it to raise an exception which I catch in my
> spec. This way I don't have to bother with either stubbing all the
> private methods called in my after_create hook, or making the mock I'm
> sending though this gauntlet respond properly to all the calls it
> gets.
>
> In my case calling RssParser.new is evidence enough that my code
> executed like it should.
>
> class SpecShortcut < Exception; end
>
>   specify "should call the standard parsing methods" do
>     RssParser.should_receive(:new).and_raise(SpecShortcut)
>     lambda{do_action}.should_raise(SpecShortcut)
>   end
>
> Feeling pain like this while writing specs is usally an indication
> that the code could be better designed. But I don't see how else one
> could spec ActiveRecord's after_create hook, without going through
> with mocking and stubbing all the code the after_create hook uses.
> Stopping execution at the point where the expectation is satisfied is
> also appealing, in a premature optimization kind of way.
>
> Like I said, an abomination, but maybe a useful one.

ActiveRecord puts us in an interesting bind with regard to the rules
that come out of TDD. Testing privates is a no-no, but testing that a
public method on the same class gets called is almost as dirty for the
same reasons. It's less likely to be a problem than private methods
because it's more stable (public methods are inherently more stable
than privates), but it still feels like a design smell - especially if
you have to intercept calls to that method (partial mocks), as you are
doing.

This is not your fault!

It is because ActiveRecord violates and/or encourages the violation
several OO design principles:

Single Responsibility Principle
Liskov Substitutiion Principle
Dependency Inversion Principle
Law of Demeter
Tell, Don't Ask

All of these principles, when applied, help you to build systems that
are highly decoupled and easy to test. When ignored you often end up
with tightly bound systems that, in spite of their up-front
productivity, are difficult to test and difficult to change over time.

Rails is difficult to test outside of the built-in testing support
that you get. If you take a look at that support, it goes a long way
to monkey patch itself into something usable in a test environment.
This is true of our own 'spec/rails' as well.

As for the ability to make changes later, try to use some other
persistence framework like Og in a Rails app and you'll see the
problem. Also, good luck trying to put plugins somewhere other than
inside your rails app.

These are trade-offs that one accepts when one uses Rails, and the
benefits we get in terms of elegant feel, up-front productivity, low
barrier to entry, etc, seem to make it worth it. My hope is that over
time the Rails code base will improve from community contributions
that make it a more highly decoupled system.

Admittedly, we have a similar problem in RSpec: you can't (yet) easily
plug in a different mocking framework because things are too tightly
bound to RSpec's own. This is something that we're looking at changing
in the near future, but there are other priorities at the forefront.
But it was an easy trap to fall into.

All of this must lead to looking at testing practices slightly
differently in the short run.

Jay Fields blogs quite a bit about solutions to problems like this.
One thing he's doing for declarations like ActiveRecord validations is
really interesting to me. You can read about it:

http://jayfields.blogspot.com/2006/12/rails-unit-testing-activerecord.html

but in a nutshell, he mocks ActiveRecord::Base telling it to expect a
declaration when a file is loaded. This solution to your problem might
look like this:

context "RssParser" do
  specify "should do x after_create" do
    Thing.should_receive(:after_create).with(:x)
    load "#{RAILS_ROOT}/app/models/rss_parser.rb"
  end
end

I'm not ready to advocate this as THE solution. This approach does
have its own problems, at least one of which is noted in a comment on
the blog. Still, I've done this in a couple of places and it has
worked well.

WDYT?

David


>
> --
> Chris Anderson
> http://jchris.mfdz.com
> _______________________________________________
> rspec-users mailing list
> rspec-users at rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>


More information about the rspec-users mailing list