[rspec-devel] Fwd: [ rspec-Bugs-6905 ] Mocking rails associations where the association is validated

David Chelimsky dchelimsky at gmail.com
Mon Nov 27 01:40:23 EST 2006


One goal of splitting specs into model, view, controller specs is to
decouple the underlying AR behaviour from what you specify in a
controller or view spec.

What I would do in the example that Pat provided is specify
validations in the model specs and then mock out the models in the
controller specs. Given your current implementation, you could have
the following spec:

specify "should create a new Person and stick it in the users people" do
  mock_user = mock("user")
  mock_person = mock("person")
  mock_people = mock("user people")
  mock_user.should_receive(:people).and_return(mock_people)
  mock_people.should_receive(:<<).with(mock_person)

  User.should_receive(:find_by_login).with("pat").and_return(mock_user)
  Person.should_receive(:new).with(:name => "BJ").and_return(mock_person)

  post :create, :user_id => "pat", :person => { :name => "BJ" }
end

If that seems like a lot of mocking to you (it does to me), that's
because adding something to @user.people in this context is a
violation of Tell, Don't Ask and also smells like Feature Envy (when
one object operates on the data of another). This could be solved by
adding an add_person method to User. Now your controller spec could
look like this:

specify "should create a new Person and stick it in the users people" do
  mock_user = mock("user")
  mock_person = mock("person")
  mock_user.should_receive(:add_person).with(mock_person)

  User.should_receive(:find_by_login).with("pat").and_return(mock_user)
  Person.should_receive(:new).with(:name => "BJ").and_return(mock_person)

  post :create, :user_id => "pat", :person => { :name => "BJ" }
end

and the create method:

def create
  @user = User.find_by_login params[:user_id]
  @person = Person.new params[:person]
  @user.add_person @person
  redirect_to person_path(@user, @person)
end

Small change to the create method.  Not so small implications in terms
of decoupling the design.

No intent to dis your design here, Pat. I've certainly done much worse
that that along the way! The problem is that ActiveRecord encourages
this sort of highly coupled  design, and the problems that go along
with it (like having to do many layers of mocking in your specs). It's
one of those situations in which we get so much good from AR that we
have to live w/ the design problems it encourages, and try to address
them through discussion and education. And that's why I believe so
strongly in the approach we're trying to take w/ the rails plugin.

Cheers,
David

---------- Forwarded message ----------
From: noreply at rubyforge.org <noreply at rubyforge.org>
Date: Nov 27, 2006 12:33 AM
Subject: [ rspec-Bugs-6905 ] Mocking rails associations where the
association is validated
To: noreply at rubyforge.org


Bugs item #6905, was opened at 2006-11-27 03:41
You can respond by visiting:
http://rubyforge.org/tracker/?func=detail&atid=3149&aid=6905&group_id=797

>Category: rails plugin
Group: None
>Status: Closed
>Resolution: Rejected
Priority: 3
Submitted By: Pat Maddox (pergesu)
>Assigned to: David Chelimsky (dchelimsky)
Summary: Mocking rails associations where the association is validated

Initial Comment:
Take the following model



class Person < ActiveRecord::Base

  belongs_to :user

  validates_presence_of :name, :user

end



the following spec



  specify "should create a new person" do

    @u.should_receive(:to_param).and_return("pat")

    @u.should_receive(:people).and_return([])

    @before_count = Person.count

    post :create, :user_id => "pat", :person => { :name => "BJ" }

    Person.should_have(@before_count + 1).records

  end



and the following action



  def create

    @user = User.find_by_login params[:user_id]

    @person = Person.new params[:person]

    @user.people << @person

    redirect_to person_path(@user, @person)

  end



The spec fails because the Person can't be saved.  The Person can't be
saved because it fails the validates_presence_of :user.  In Rails, the
collection is a proxy rather than simply an array, and the << method
is written such that it would actually set @person.user in the above
code.  A simple array of course doesn't do that.



I've got an idea to handle this, which involves writing a basic class
that does all the setting, and returning that for people instead of an
array.  That ought to work.



The code behaves as expected, but the spec does not.



The other way you would do this association is to do

@person.user = @user



but Rails does some type checking here, and you get

User expected, got Spec::Mocks::Mock



No problem, just do

@user.should_receive(:class).and_return(User)

BUT you get

User expected, got User



hrm.  I made it return the string "User" instead of the class and got
the same thing.



I'm going to work on a rails-like collection class, since that's what
I'm most interested in at the moment.  Not sure what to do about the
strange "User expected, got User" when assigning the value of a
belongs_to.

----------------------------------------------------------------------

>Comment By: David Chelimsky (dchelimsky)
Date: 2006-11-27 06:33

Message:
This is not an RSpec bug. You are choosing to return an Array instead
of the Rails proxy (or a mock of it) which is how Rails behaves.



I'm going to follow up with this on the mailing list.



Cheers,

David

----------------------------------------------------------------------

You can respond by visiting:
http://rubyforge.org/tracker/?func=detail&atid=3149&aid=6905&group_id=797


More information about the rspec-devel mailing list