Forum: RSpec Factories vs. stubs/mocks

Posted by Brennon Bortz (Guest)
on 2010-08-30 18:00
(Received via mailing list)
I am, as usual, assigning an instance variable in a controller's index 
action to find all instances of a model in the database.  I want to 
write a spec that checks that this variable is assigned correctly.  I 
can do:

it "should provide a collection of widgets in @widgets" do
      widget = Widget.create("title" => "my widget")
      get :index
      assigns[:widgets].should include(widget)
end

or:

it "should provide a collection of widgets in @widgets" do
      widget = Factory.create(:widget)
      get :index
      assigns[:widgets].should include(widget)
end

Is there a better way to do this with a mock model, though?

Thanks,

Brennon Bortz
Software Researcher
Dundalk Institute of Technology
brennon.bortz@casala.ie
Ph.D. Researcher & Composer - Sonic Arts Research Centre
Queen's University, Belfast
brennon@brennonbortz.com / bbortz01@qub.ac.uk
Posted by Justin Ko (Guest)
on 2010-08-30 18:29
(Received via mailing list)
On Aug 30, 11:59 am, Brennon Bortz <bren...@brennonbortz.com> wrote:
> it "should provide a collection of widgets in @widgets" do
> Software Researcher
> Dundalk Institute of Technology
> brennon.bo...@casala.ie
> Ph.D. Researcher & Composer - Sonic Arts Research Centre
> Queen's University, Belfast
> bren...@brennonbortz.com / bbort...@qub.ac.uk
>
> _______________________________________________
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

Currently, what you're doing is checking that the Widget model returns
the correct widgets. If you want to isolate your controller spec from
the model, you must stub or mock the model method call in the action.
Example:

it "..." do
  widget = mock_model(Widget)
  Widget.should_receive(:all).and_return([widget])
  get :index
  assigns[:widgets].should include(widget)
end

To check that Widget.all does indeed return the correct widgets, I
would spec that behaviour in the Widget model spec.

Hope that helps.
Posted by Brennon Bortz (Guest)
on 2010-08-30 19:00
(Received via mailing list)
On 30 Aug 2010, at 17:17, Justin Ko wrote:

>> 
>> Thanks,
>> rspec-users mailing list
>  get :index
>  assigns[:widgets].should include(widget)
> end
> 
> To check that Widget.all does indeed return the correct widgets, I
> would spec that behaviour in the Widget model spec.
> 
> Hope that helps.

Hrm...that's exactly what I'd started with, and I was getting the 
following error:

Failures:
  1) WidgetsController GET 'index' should provide a collection of 
widgets in @widgets
     Failure/Error: assigns[:widgets].should include(widget)
     expected [] to include #<Widget:0x81686290 @name="Widget_1001">

Stupidly, I had defined my controller method as:

def index
  @widgets = Widget.find(:all)
end

Changed that assignment to Widget.all...problem solved.
Posted by Justin Ko (Guest)
on 2010-08-30 19:07
(Received via mailing list)
On Aug 30, 12:54 pm, Brennon Bortz <bren...@brennonbortz.com> wrote:
>
> >>       get :index
> >> brennon.bo...@casala.ie
> > the model, you must stub or mock the model method call in the action.
> > would spec that behaviour in the Widget model spec.
> Stupidly, I had defined my controller method as:
>
> def index
>   @widgets = Widget.find(:all)
> end
>
> Changed that assignment to Widget.all...problem solved.
> _______________________________________________
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

To mock a method that receives an argument, you can use the "with"
method. Example:

Widget.should_receive(:find).with(:all).and_return([widget])

Glad you got things fixed :)
Posted by Rob Biedenharn (Guest)
on 2010-08-30 19:10
(Received via mailing list)
On Aug 30, 2010, at 12:54 PM, Brennon Bortz wrote:
>>>      assigns[:widgets].should include(widget)
>>> Is there a better way to do this with a mock model, though?
>>>
>> it "..." do
>
>
> def index
>  @widgets = Widget.find(:all)
> end
>
> Changed that assignment to Widget.all...problem solved.

And THAT is the problem with using mocks (or stubs) for this.  You run
the very real risk of specifying the implementation details in the
structure of the spec/test.  There is a problem when you've limited
the refactoring that can be done without altering the spec/test. Since
Widget.find(:all) and Widget.all should always have the same result,
the mere presence of the mock has cut off one possible refactoring.
(The one that I distaste most limits changing .find(params[:id])
to .find_by_id(params[:id]) by mocking/stubbing the call to find.)

Think about what really needs to be specified and be careful to mock/
stub as little as possible and to do so in a way that will limit
opportunities to refactor the least.

-Rob

Rob Biedenharn
Rob@AgileConsultingLLC.com  http://AgileConsultingLLC.com/
rab@GaslightSoftware.com    http://GaslightSoftware.com/
Posted by Justin Ko (Guest)
on 2010-08-30 19:32
(Received via mailing list)
On Aug 30, 1:09 pm, Rob Biedenharn <R...@AgileConsultingLLC.com>
wrote:
> >>> want to write a spec that checks that this variable is assigned  
> >>> it "should provide a collection of widgets in @widgets" do
> >>> Software Researcher
> >> Currently, what you're doing is checking that the Widget model  
> >> end
> >  1) WidgetsController GET 'index' should provide a collection of  
> > Changed that assignment to Widget.all...problem solved.
> Think about what really needs to be specified and be careful to mock/
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

The method you suggested does benefit refactoring. However, by not
stubbing or mocking, your controller is no longer in isolation. Which
means if Widget.all breaks, the model AND controller spec will fail.

The are pros and cons to both ways. But what has pushed me to the
isolation side is that without mocking/stubbing, you must create a
record in the database (via fixtures or factories) and it is much
slower.
Posted by Justin Ko (Guest)
on 2010-08-30 20:26
(Received via mailing list)
On Aug 30, 1:32 pm, Justin Ko <jko...@gmail.com> wrote:
> > >> On Aug 30, 11:59 am, Brennon Bortz <bren...@brennonbortz.com> wrote:
>
> > >>> Thanks,
> > >>> rspec-users mailing list
> > >> Widget.should_receive(:all).and_return([widget])
> > > following error:
> > >  @widgets = Widget.find(:all)
> > (The one that I distaste most limits changing .find(params[:id])  
> > r...@GaslightSoftware.com            http://GaslightSoftware.com/
> isolation side is that without mocking/stubbing, you must create a
> record in the database (via fixtures or factories) and it is much
> slower.
> _______________________________________________
> rspec-users mailing list
> rspec-us...@rubyforge.orghttp://rubyforge.org/mailman/listinfo/rspec-users

Just remembered another reason why I chose isolation. Sometimes, the
setup to NOT isolate can be a real pain. a.k.a creating factories and
relationships.

But, let's say you've something like this in your controller:

User.where(:email =>
'test@blah.com').includes(:widgets).order('users.email ASC').limit(3)

Simply stubbing or mocking that out would not spec the SQL very well.
So what I usually do is extract it to the model:

User.widgets_by_email('test@blah.com')

# in model

def self.widgets_by_email(email)
  where(:email => email).includes(:widgets).order('users.email
ASC').limit(3)
end

After doing that, I feel comfortable mocking out
"User.widgets_by_email" in the controller, because all I care about is
if its called or not.

And of course, since the method has been moved to the model, you can
unit spec the hell out of it :)
Posted by Matt Wynne (mattwynne)
on 2010-08-31 11:16
(Received via mailing list)
On 30 Aug 2010, at 18:32, Justin Ko wrote:

>>> On 30 Aug 2010, at 17:17, Justin Ko wrote:
>>>>> end
>> 
>>>>> _______________________________________________
>>>> widget = mock_model(Widget)
>>> Hrm...that's exactly what I'd started with, and I was getting the  
>>> def index
>> the mere presence of the mock has cut off one possible refactoring.  
>> R...@AgileConsultingLLC.com  http://AgileConsultingLLC.com/
> The are pros and cons to both ways. But what has pushed me to the
> isolation side is that without mocking/stubbing, you must create a
> record in the database (via fixtures or factories) and it is much
> slower.

Personally, I would use this as a driver to create a new method on 
Widget which did exactly what this controller wants, wrapping the 
details of the ActiveRecord API. That way, I have a nice, readable 
controller with fast, isolated, specs; a clear API on my models that 
makes sense in my domain; and if I want to change from a relational DB 
to a key-value store, I don't have a dependency on ActiveRecord leaking 
out of my model all over the place.

> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users

cheers,
Matt

http://blog.mattwynne.net
+44(0)7974 430184
Posted by J. B. Rainsberger (Guest)
on 2010-09-06 16:39
(Received via mailing list)
On Mon, Aug 30, 2010 at 14:09, Rob Biedenharn
> And THAT is the problem with using mocks (or stubs) for this.  You run the
> very real risk of specifying the implementation details in the structure of
> the spec/test.  There is a problem when you've limited the refactoring that
> can be done without altering the spec/test. Since Widget.find(:all) and
> Widget.all should always have the same result, the mere presence of the mock
> has cut off one possible refactoring.  (The one that I distaste most limits
> changing .find(params[:id]) to .find_by_id(params[:id]) by mocking/stubbing
> the call to find.)

On the contrary: by stubbing you've uncovered that you had used a
low-level API (#find) instead of a high-level API (#all). This
prompted you to make a conscious choice, which you did, and I prefer
the choice you made, to invoke #all instead of #find.

…and THAT is the magic of using stubs and expectations for this.
--
J. B. (Joe) Rainsberger :: http://www.jbrains.ca ::
http://blog.thecodewhisperer.com
Diaspar Software Services :: http://www.diasparsoftware.com
Author, JUnit Recipes
2005 Gordon Pask Award for contribution to Agile practice :: Agile
2010: Learn. Practice. Explore.
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.