[rspec-devel] stub_model

Zach Dennis zach.dennis at gmail.com
Fri Mar 21 11:30:47 EDT 2008


On Wed, Mar 19, 2008 at 9:16 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
> On Tue, Mar 18, 2008 at 10:45 PM, Zach Dennis <zach.dennis at gmail.com> wrote:
>  > On Tue, Mar 18, 2008 at 9:10 AM, David Chelimsky <dchelimsky at gmail.com> wrote:
>  >  > Hi all,
>  >  >
>  >  >  Over the last couple of years I've read a ton of mail from users
>  >  >  concerned with false positives coming from stubbing/mocking methods
>  >  >  that don't exist. I recently added a stub_model method for
>  >  >  rspec_on_rails (not yet released, available in git) which I think
>  >  >  mitigates this a bit. It is still in development and subject to
>  >  >  change, but here's how it works now.
>  >  >
>  >  >  It looks a lot like mock_model.
>  >  >
>  >  >   stub_model(Person, :name => 'David')
>  >  >
>  >  >  But it works in a fundamentally different way: FIrst, it creates a
>  >  >  real instance (which means you have to create the model to use it). It
>  >  >  assigns it an id by default, but you can set :id => nil if you want it
>  >  >  to behave like a new record. It overrides new_record? so that it
>  >  >  behaves as you would expect (false if there is an id, true if not). It
>  >  >  also overrides #connection, raising an error if there is any attempt
>  >  >  to access the database. This gives you the db isolation you get from
>  >  >  mock_model, but with a real object.
>  >
>  >  Does this mean you have access to real model methods? If I define
>  >  FooModel#bar and I use stub_model(Foo) in a controller and someone
>  >  updates the controller to call foo#bar will it complain that an
>  >  unexpected method was called or will it call the real foo#bar method?
>
>  The object IS a real model, so yes, it provides access to all of the
>  model methods.
>
>
>  >  I hope it blows up, otherwise it acts like partial mocking classes and
>  >  that has negative drawbacks.
>
>  It would fail if accessing that method caused some sort of error.
>
>
>  > Side rant: IMO partial mocking is evil
>  >  and should be avoided when they can be.
>
>  In general I agree, which is why it has taken me this long to arrive
>  at this solution. The motivation for me, personally, is that with a
>  mock object my view specs end up having to explicitly provide a lot of
>  stub values that the examples are not interested. They are just noise
>  that is present to keep the view moving.
>
>  It also strikes me that view specs are inherently state-based.
>
>  Take this for example:
>
>  describe "/people/show.html.erb" do
>   ...
>   it "should show the person's full name" do
>     assigns[:person] = stub_model(Person, :full_name => "David Chelimsky")
>     do_render
>     response.should have_tag(".name", "David Chelimsky")
>   end
>  end
>
>  To me, this is a very clear and simple example. We could debate that
>  it should be an interaction test, using
>  should_receive(:full_name).and_return(".."), but regardless of style
>  and intent, anyone with experience with view specs can read this and
>  it is very clear what is being expressed.
>
>  So let's say we add the person's email to the view:
>
>   it "should show the person's email" do
>     assigns[:person] = stub_model(Person, :email => "a at b.com")
>     do_render
>     response.should have_tag(".email", "a at b.com")
>   end
>
>  With a mock object, both of these examples would now fail and I'd be
>  forced to supply both attributes for both examples. Sure, I could do
>  that by using a factory method (or object), but now I have to do this:
>
>   it "should show the person's email" do
>     assigns[:person] = create_person
>     do_render
>     response.should have_tag(".email", "a at b.com")
>   end
>
>  But then if I change the value of the email address in the factory,
>  this example fails and I can't just look right at it to understand the
>  failure, I have to go look at the factory. So maybe I change the
>  expectation to use the created person's email.
>
>   it "should show the person's email" do
>     person = create_person
>     assigns[:person] = person
>     do_render
>     response.should have_tag(".email", person.email)
>   end
>
>  Except now, if the person's email is nil, and the tag happens to have
>  nil, the example will pass when it should fail. So maybe my factory
>  method takes arguments:
>
>   it "should show the person's email" do
>     assigns[:person] = create_person(:email => "a at b.com")
>     do_render
>     response.should have_tag(".email", "a at b.com")
>   end
>
>  Now things are explicit and I'm still using a mock. Perhaps this is a
>  good solution using a mock object, but as the views grow, and we all
>  know they do, this becomes more and more work to maintain.
>
>  Using a real model instance and mocking/stubbing only what I expect in
>  a given example has allowed me to keep things much simpler in the view
>  specs and so far they have caused me no pain. Perhaps the lack of pain
>  has to do with the fact that views are generally not "interacting"
>  with the model, per se. They are simply grabbing and displaying
>  values. They are asking, not telling. Based on that, a mock object
>  almost seems like a waste.
>
>
>  > They clutter up tests with
>  >  cases that shouldn't be there, but have to be there in order to ensure
>  >  certain calls aren't made (where a real mock would yell at you for
>  >  calling a method you didn't stub or expect). End side rant. =)
>
>  I agree with you here in most cases. I'm coming to believe that views,
>  specifically views that rely on getters as Rails views do, are an edge
>  case when it comes to mock objects.
>
>  Consider an example from a CMS I'm working on. There is a content item
>  named promo, which has a bunch of attributes and near-zero behaviour.
>  Take a look at the code using mock_model (which wraps a mock object)
>  vs the code using stub_model (which wraps a real model object):
>  http://pastie.caboo.se/167751.
>
>  I think you'll agree that, given that the example is only interested
>  in form fields and not values, the latter is far superior for a number
>  of reasons. It is more clear. There is less noise. It is less brittle.
>  I'm sure there are others.

I agree with basically everything you've explained. In your example
you made the mock_model more verbose then it should have to be:

  before(:each) do
    @promo = mock_model(Promo)
    @promo.stub!(:new_record?).and_return(true)
    @promo.stub!(:title).and_return("MyString")
    @promo.stub!(:slug).and_return("MyString")
    @promo.stub!(:source).and_return("MyString")
    @promo.stub!(:author).and_return("MyString")
    @promo.stub!(:date).and_return(Date.today)
    @promo.stub!(:link).and_return("MyString")
    @promo.stub!(:text).and_return("MyText")
    @promo.stub!(:new_window).and_return(false)
    assigns[:promo] = @promo
  end

Couldn't that just be:

  @promo = mock_model(Promo,
    :new_record? => true,
    :title => "MyString",
    :slug => "MyString",
    etc... )

Not that it makes a huge improvement, but it cuts down on the noise
and increases the clarity. With the example you gave using stub_model
definitely seems to have have advantage especially since Rails forms
are so tightly integrated with their ActiveRecord counterparts.

Have you used stub_model outside of form testing, and if so how do you
find it working their?

-- 
Zach Dennis
http://www.continuousthinking.com


More information about the rspec-devel mailing list