RSpec test for create action of a controller for a nested resource

10,836

For a nested resource you need to construct the setup data and the post in such a way as to identify the parent article when posting the child comment.

One approach is to setup Factory Girl associations correctly and then ensure the parent element is set when creating the child attributes. It would look something like this:

In the comment factory:

FactoryGirl.define do
  Factory :comment do
    comment "My comment"
    article
  end
end

By calling article, and making sure that there is a valid factory called :article then FactoryGirl will create an article when a comment is created. To make the tests flow well we should actually be specific about which article is used when the comment is created, so now that the Factory is in place we use the following in the spec.

@comment_attributes = FactoryGirl.attributes_for(:comment, :article_id => @article)

This will build comment attributes which are automatically attached to @article. The final piece is then to construct the post, making sure that we include the parent and the child.

When a nested resource is posted it expects params for both the parent resource and the child. In rspec we can provide this in the post as follows:

post :create, :article_id => @article, :comment => @comment_attributes

This should link up all the pieces correctly.

Share:
10,836
Patrick Frey
Author by

Patrick Frey

Doing Ruby on Rails development with deployment to Heroku!

Updated on June 17, 2022

Comments

  • Patrick Frey
    Patrick Frey over 1 year

    I have a Rails application (Rails 3.0.10) where users can have many articles, and where the users can leave comments on the articles. Comments are made on the article show page.

    Now I want to test the create action of the CommentsController, however, I have problems of invoking the post method with the right parameters.

    Here's the code of the CommentsController:

    class CommentsController < ApplicationController
    
      # create a comment and bind it to an article and a user  
      def create
        @article = Article.find(params[:article_id])
        @user = User.find(@article.user_id)
        @comment = @article.comments.build(params[:comment])
        @comment.user_id = current_user.id
    
        commenters = [] 
        @article.comments.each {
          |comment|
          commenters << User.find(comment.user_id)
        }
        commenters.uniq!
    
        respond_to do |format|
          if @comment.save        
    
            #Notify user who offers article on new comment, else notify the commenters
            if @article.user_id != @comment.user_id
              UserMailer.new_article_comment_email(@user, @comment).deliver
            else        
              commenters.each {
                |commenter|
                UserMailer.new_article_comment_email(commenter, @comment).deliver
              }
            end
    
            format.html { 
              redirect_to(@article)
              flash[:notice] = t(:comment_create_success)
            }
          else
            format.html { 
              redirect_to(@article) 
              flash[:error] = t(:comment_create_error)
            }
          end
        end
      end
    end
    

    The RSpec code for testing this action (some experiments so far) is the following:

    require 'spec_helper'
    require 'ruby-debug'
    
    describe CommentsController do
      render_views
    
      describe "POST 'create'" do
    
        before(:each) do
          @user = FactoryGirl.create(:user)
    
          @article = FactoryGirl.build(:article)
          @article.user_id = @user.id
          @article.save
    
          @article_attributes = FactoryGirl.attributes_for(:article)
          @comment_attributes = FactoryGirl.attributes_for(:comment)
        end
    
        it "should create a new comment" do
          expect {
            post :create, :comment => @comment_attributes
          }.to change(Comment, :count).by(1)
        end
    
        it "should create a new comment, redirect to the article show page of this comment and notify the user on successful saving of the comment" do
          post :create, :comment => @comment_attributes, :article_id => @article.id.to_s, :user_id => @user.id.to_s
          flash[:notice].should_not be_nil
          response.should redirect_to(article_path(@article))
        end
    
      end
    
    end
    

    Both tests fail, however, due to different reasons that I am unable to fix:

        Failures:
    
          1) CommentsController POST 'create' should create a new comment
             Failure/Error: post :create, :comment => @comment_attributes
             ActionController::RoutingError:
               No route matches {:comment=>{:body=>"This is the body text of a comment"}, :controller=>"comments", :action=>"create"}
             # ./spec/controllers/comments_controller_spec.rb:22:in `block (4 levels) in <top (required)>'
             # ./spec/controllers/comments_controller_spec.rb:21:in `block (3 levels) in <top (required)>'
    
          2) CommentsController POST 'create' should create a new comment, redirect to the article show page of this comment and notify the user on successful saving of the comment
             Failure/Error: post :create, :comment => @comment_attributes, :article_id => @article.id.to_s, :user_id => @user.id.to_s
             RuntimeError:
               Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
             # ./app/controllers/comments_controller.rb:8:in `create'
             # ./spec/controllers/comments_controller_spec.rb:27:in `block (3 levels) in <top (required)>'
    

    I would be great if someone could help me out. Thanks in advance!

    Update: Here's the routes.rb I am using:

    Cinderella::Application.routes.draw do
    
      # The priority is based upon order of creation:
      # first created -> highest priority.
    
      # Sample of regular route:
      #   match 'products/:id' => 'catalog#view'
      # Keep in mind you can assign values other than :controller and :action
    
      # Sample of named route:
      #   match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
      # This route can be invoked with purchase_url(:id => product.id)  
    
      match '/signup',  :to => 'users#new'
      match '/signin',  :to => 'sessions#new'
      match '/signout',  :to => 'sessions#destroy'
    
      match '/home', :to => 'pages#home'
      match '/about',   :to => 'pages#about'
      match '/faq', :to => 'pages#faq'
      match '/howitworks_sellers', :to => "pages#howitworks_sellers"
      match '/howitworks_buyers', :to => "pages#howitworks_buyers"
      match '/contact', :to => 'pages#contact'
    
      match '/articles/:id/ratings', :to => 'ratings#destroy'
    
      # Sample resource route (maps HTTP verbs to controller actions automatically):
      #   resources :products
    
      resources :articles do
        resources :comments, :only => [:create, :destroy]
      end
    
      resources :ratings
      resources :ratings do
        collection do
          post 'destroy'
        end
      end
    
      resources :users do
        resources :articles
      end
    
      resources :sessions, :only => [:new, :create, :destroy]
    
      # Sample resource route with options:
      #   resources :products do
      #     member do
      #       get 'short'
      #       post 'toggle'
      #     end
      #
      #     collection do
      #       get 'sold'
      #     end
      #   end
    
      # Sample resource route with sub-resources:
      #   resources :products do
      #     resources :comments, :sales
      #     resource :seller
      #   end
    
      # Sample resource route with more complex sub-resources
      #   resources :products do
      #     resources :comments
      #     resources :sales do
      #       get 'recent', :on => :collection
      #     end
      #   end
    
      # Sample resource route within a namespace:
      #   namespace :admin do
      #     # Directs /admin/products/* to Admin::ProductsController
      #     # (app/controllers/admin/products_controller.rb)
      #     resources :products
      #   end
    
      # You can have the root of your site routed with "root"
      # just remember to delete public/index.html.
      root :to => "pages#home"
    
      # See how all your routes lay out with "rake routes"
    
      # This is a legacy wild controller route that's not recommended for RESTful applications.
      # Note: This route will make all actions in every controller accessible via GET requests.
      # match ':controller(/:action(/:id(.:format)))'
    end
    #== Route Map
    # Generated on 14 Dec 2011 14:24
    #
    #            signin        /signin(.:format)                           {:controller=>"sessions", :action=>"new"}
    #           signout        /signout(.:format)                          {:controller=>"sessions", :action=>"destroy"}
    #              home        /home(.:format)                             {:controller=>"pages", :action=>"home"}
    #             about        /about(.:format)                            {:controller=>"pages", :action=>"about"}
    #               faq        /faq(.:format)                              {:controller=>"pages", :action=>"faq"}
    #          articles GET    /articles(.:format)                         {:action=>"index", :controller=>"articles"}
    #                   POST   /articles(.:format)                         {:action=>"create", :controller=>"articles"}
    #       new_article GET    /articles/new(.:format)                     {:action=>"new", :controller=>"articles"}
    #      edit_article GET    /articles/:id/edit(.:format)                {:action=>"edit", :controller=>"articles"}
    #           article GET    /articles/:id(.:format)                     {:action=>"show", :controller=>"articles"}
    #                   PUT    /articles/:id(.:format)                     {:action=>"update", :controller=>"articles"}
    #                   DELETE /articles/:id(.:format)                     {:action=>"destroy", :controller=>"articles"}
    #     user_articles GET    /users/:user_id/articles(.:format)          {:action=>"index", :controller=>"articles"}
    #                   POST   /users/:user_id/articles(.:format)          {:action=>"create", :controller=>"articles"}
    #  new_user_article GET    /users/:user_id/articles/new(.:format)      {:action=>"new", :controller=>"articles"}
    # edit_user_article GET    /users/:user_id/articles/:id/edit(.:format) {:action=>"edit", :controller=>"articles"}
    #      user_article GET    /users/:user_id/articles/:id(.:format)      {:action=>"show", :controller=>"articles"}
    #                   PUT    /users/:user_id/articles/:id(.:format)      {:action=>"update", :controller=>"articles"}
    #                   DELETE /users/:user_id/articles/:id(.:format)      {:action=>"destroy", :controller=>"articles"}
    #             users GET    /users(.:format)                            {:action=>"index", :controller=>"users"}
    #                   POST   /users(.:format)                            {:action=>"create", :controller=>"users"}
    #          new_user GET    /users/new(.:format)                        {:action=>"new", :controller=>"users"}
    #         edit_user GET    /users/:id/edit(.:format)                   {:action=>"edit", :controller=>"users"}
    #              user GET    /users/:id(.:format)                        {:action=>"show", :controller=>"users"}
    #                   PUT    /users/:id(.:format)                        {:action=>"update", :controller=>"users"}
    #                   DELETE /users/:id(.:format)                        {:action=>"destroy", :controller=>"users"}
    #          sessions POST   /sessions(.:format)                         {:action=>"create", :controller=>"sessions"}
    #       new_session GET    /sessions/new(.:format)                     {:action=>"new", :controller=>"sessions"}
    #           session DELETE /sessions/:id(.:format)                     {:action=>"destroy", :controller=>"sessions"}
    #              root        /(.:format)                                 {:controller=>"pages", :action=>"home"}
    

    Update: Here's the modification I did according to nmotts suggestions:

    require 'spec_helper'
    require 'ruby-debug'
    
    describe CommentsController do
      render_views
    
      describe "POST 'create'" do
    
        before(:each) do
          @user = FactoryGirl.create(:user)
    
          @article = FactoryGirl.build(:article)
          @article.user_id = @user.id
          @article.save
    
          @comment_attributes = FactoryGirl.attributes_for(:comment, :article_id => @article)
        end
    
        it "should create a new comment" do
          post :create, :article_id => @article.id.to_s, :comment => @comment_attributes
        end
    
      end
    
    end
    

    And the FactoryGirl definition for comment:

    factory :comment do
      body "This is the body text of a comment"
      article
    end
    

    Unfortunately, the code is not yet working.

  • Patrick Frey
    Patrick Frey almost 12 years
    Thanks nmott, with your explanations things get clearer to me. Unfortunately, it did not work out yet. I posted my changes according to your recommendations above.
  • Patrick Frey
    Patrick Frey almost 12 years
    OK, I got it working now. The problem was that only signed in users can make comments. The problem was thus not only how to test the nested controller, but it was necessary to create a test user and log the user in before testing the nested comment controller.