[rspec-devel] stub_model

Zach Dennis zach.dennis at gmail.com
Fri Mar 21 11:32:18 EDT 2008


On Fri, Mar 21, 2008 at 11:30 AM, Zach Dennis <zach.dennis at gmail.com> wrote:
>
> 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?
>

"their".sub /ir/, /re/


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

P.S. - didn't mean to spam you David, I hit reply instead of reply to
all the first time


More information about the rspec-devel mailing list