30
Sep
RSpec Tutorial Part 4 - Mocking
Mocking
Consider the following controller action:
def comment @post = Post.find(params[:id]) @comment = Comment.new(params[:comment]) respond_to do |format| if @comment.save && @post.comments.concat(@comment) flash[:notice] = 'Comment was successfully updated.' format.html { redirect_to post_path(@blogger, @post) } format.xml { head :ok } else flash[:notice] = 'Error saving comment.' format.html { redirect_to post_path(@blogger, @post) } format.xml { render :xml => @comment.errors.to_xml } end end end
I need to test that I'm able to comment on posts, so I could do this:
it "should add comments to the post" do post = posts(:one) comment_count = post.comments.size post :comment, :id => 1, :comment => {:title => 'test', :body => 'very nice!'} post.reload post.comments.size.should == comment_count + 1 flash[:notice].should match(/success/i) response.should redirect_to(posts_path(@blogger, @post)) end
The problem with this test is that I'm testing much more than I need to. I'm testing functionality that I've already tested in the models. Even worse, I'm dependent upon the upkeep of that fixture file.
I'll use a fake post instead:
post = mock_model(Post)
The next thing I need to worry about is the first line in the comment action:
@post = Post.find(params[:id])
In testing just the functionality of the controller, I want to make sure this line is executed, but I can assume the method itself works. The Rails team has already tested that much better than I can. I need to intercept that call and return my mocked model. There are two different ways to do this:
Post.should_receive(:find).and_return(post)
Or:
Post.stub!(:find).and_return(post)
The difference between stubbing the find method and mocking it is huge here. If I use '.stub!', then Post will return my mock post if the .find method is called. If it is not called my stub won't care at all. If I mock it with 'should_receive' then it will care: It will raise an error. Think of 'should_receive' as a whiny stub.
The 'and_return(post)' does exactly the same thing in each instance: it returns whatever you pass to it when your mocked or stubbed method is called.
Next, I want to make sure that '.new' is called on Comment with the attributes I'm sending via params.
comment = mock_model(Comment) Comment.should_receive(:new).with({:title => 'comment title', :body => 'comment body'}).and_return(comment)
Now I'm getting to the good part. I have to satisfy the necessary conditions for the following expressions in my action:
if @comment.save && @post.comments.concat(@comment)
Now I'm going to build out my comment mock:
comment.should_receive(:save).and_return(true) post.comments.should_receive(:concat).and_return(comment)
Excellent, now I've mocked enough to get to where I can test my flash and the redirect:
flash[:notice].should match(/success/i) response.should redirect_to(posts_path(blogger, post))
There's one last thing, the action has to have a blogger to redirect properly. I'll use instance_variable_set on the controller to do that.
blogger = mock_model(User) @controller.instance_variable_set(:@blogger, blogger)
Here's what the entire spec looks like. I've separated out the preparatory work into a before section of the spec:
describe PostsController, "comment", :behaviour_type => :controller, do before do @post = mock_model(Post) Post.should_receive(:find).and_return(@post) @comment = mock_model(Comment) Comment.should_receive(:new).with.({:title => 'comment title', :body => 'comment body'}).and_return(@comment) @blogger = mock_model(User) @controller.instance_variable_set(:@blogger, @blogger) end it "should add comments to the post" do @comment.should_receive(:save).and_return(true) @post.comments.should_receive(:concat).and_return(@comment) post :comment, :id => @post.id, :comment => {:title => 'comment title', :body => 'comment body'} flash[:notice].should match(/success/i) response.should redirect_to(posts_path(@blogger, @post)) end end
