I'm trying to test the actions in controller spec but for some reason I get the no routes matches error. What should I do to make the route work?
ActionController::UrlGenerationError:
No route matches {:action=>"create", :comment=>{:body=>"Consectetur quo accusamus ea.",
:commentable=>"4"}, :controller=>"comments", :post_id=>"4"}
model
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true, touch: true
class Post < ActiveRecord::Base
has_many :comments, as: :commentable, dependent: :destroy
routes
resources :posts do
resources :comments, only: [:create, :update, :destroy], module: :posts
end
controller_spec
describe "POST create" do
let!(:user) { create(:user) }
let!(:profile) { create(:profile, user: #user) }
let!(:commentable) { create(:post, user: #user) }
context "with valid attributes" do
subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: #user) }
it "saves the new task in the db" do
expect{ create_action }.to change{ Comment.count }.by(1)
end
...
EDIT
The controller_spec from above can be found in spec/controllers/comments_controller_spec.rb
controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :authenticate_user!
def create
#comment = #commentable.comments.new(comment_params)
authorize #comment
#comment.user = current_user
if #comment.save
#comment.send_comment_creation_notification(#commentable)
respond_to :js
end
end
controllers/posts/comments_controller.rb
class Posts::CommentsController < CommentsController
before_action :set_commentable
private
def set_commentable
#commentable = Post.find(params[:post_id])
end
Using the module: :posts will route to Posts::CommentsController#create.
If that is not what you intended than remove the module option.
Otherwise you need to ensure that you have the correct class name for both your controller and spec.
class Posts::CommentsController
def create
end
end
RSpec.describe Posts::CommentsController do
# ...
end
Also note that if often does not make sense to nest the "individual actions" for a resource.
Instead you may want to declare the routes like so:
resources :comments, only: [:update, :destroy] # show, edit ...
resources :posts do
resources :comments, only: [:create], module: :posts # new, index
end
Which gives you:
class CommentsController < ApplicationController
before_action :set_posts
# DELETE /comments/:id
def destroy
# ...
end
# PUT|PATCH /comments/:id
def update
end
end
class Posts::CommentsController < ApplicationController
# POST /posts/:post_id/comments
def create
# ...
end
end
See Avoid Deeply Nested Routes in Rails for a deeper explaination of why.
Setting the up the controller to use inheritance in this case is a good idea - however you cannot test the create method through the parent CommentsController class in a controller spec since RSpec will always look at described_class when trying to resolve the route.
Instead you may want to use shared examples:
# /spec/support/shared_examples/comments.rb
RSpec.shared_examples "nested comments controller" do |parameter|
describe "POST create" do
let!(:user) { create(:user) }
context "with valid attributes" do
subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: #user) }
it "saves the new task in the db" do
expect{ create_action }.to change{ Comment.count }.by(1)
end
end
end
end
require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Posts::CommentsController
# ...
include_examples "nested comments controller" do
let(:commentable) { create(:post, ...) }
end
end
require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Products::CommentsController
# ...
include_examples "nested comments controller" do
let(:commentable) { create(:product, ...) }
end
end
The other alternative which I prefer is to use request specs instead:
require 'rails_helper'
RSpec.describe "Comments", type: :request do
RSpec.shared_example "has nested comments" do
let(:path) { polymorphic_path(commentable) + "/comments" }
let(:params) { attributes_for(:comment) }
describe "POST create" do
expect do
xhr :post, path, params
end.to change(commentable.comments, :count).by(1)
end
end
context "Posts" do
include_examples "has nested comments" do
let(:commentable) { create(:post) }
end
end
context "Products" do
include_examples "has nested comments" do
let(:commentable) { create(:product) }
end
end
end
Since you are really sending a HTTP request instead of faking it they cover more of the application stack. This does however come with a small price in terms of test speed. Both shared_context and shared_examples are two of the things which make RSpec really awesome.
Related
I have updated the code spec code.
What I seek is to destroy a record only with the same user that has created it.
I've tried in the view section and it seems to be working, but the Rspec is throwing me some errors.
Can anyone please tell me how to do a correct spec?
Thanks
My Record model:
class Record < ActiveRecord::Base
#Associations
belongs_to :user
# Validations
validates :user, presence: true
end
My Record factory:
FactoryGirl.define do
factory :record do
user
end
end
My Record controller:
class RecordsController < ApplicationController
before_action :find_record, only: [:show, :edit, :update, :destroy]
before_action :require_permission, only: [:destroy]
def destroy
#record.destroy
flash[:notice] = "The record was deleted successfully"
redirect_to #record
end
private
def require_permission
if current_user != Record.find(params[:id]).user
flash[:notice] = "Permission required"
redirect_to root_path
end
end
end
My record spec:
require 'rails_helper'
describe RecordsController do
let(:record) { create(:record) }
let(:user) { create(:user) }
describe "#destroy" do
let!(:record) { create(:record) }
#UPDATED
login_user
it "deletes the record" do
expect {
delete :destroy, id: record.id, :record => {:user => record.user}
}.to change(Record, :count).by(-1)
expect(flash[:notice]).to eq("The record was deleted successfully")
end
end
end
UPDATE 2
rails_helper.rb
require 'spec_helper'
require 'devise'
RSpec.configure do |config|
config.include Devise::TestHelpers, type: :controller
config.extend ControllerMacros, type: :controller
end
at spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in FactoryGirl.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
#user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
end
My errors:
#destroy
deletes the record (FAILED - 1)
Failures:
1) RecordsController#destroy deletes the record
Failure/Error: expect {
expected #count to have changed by -1, but was changed by 0
you use #record but you let record that's why in error #record is nil
it "deletes the record" do
expect {
delete :destroy, id: record.id, :record => {:user => record.user}
}.to change(Record, :count).by(-1)
expect(flash[:notice]).to eq("The record was deleted successfully")
end
My "gets edit" and "destroys tests" keep failing and I do not understand why. I think it has to do with the correct_user method in my controller.
For the destroy method it says that the contest.count did not change by -1. The edit method test just dumps everything there is to dump. (no real error message)
Does anybody have an idea how I can fix this?
require "test_helper"
describe ContestsController do
let(:user) { users :default }
let(:contest) { contests :one }
it "gets index" do
get :index
value(response).must_be :success?
value(assigns(:contests)).wont_be :nil?
end
describe "gets new" do
it "redirects to login_path" do
session[:user_id] = nil
get :new
assert_redirected_to new_user_session_path
end
it "requires authentication" do
sign_in users :default
get :new
value(response).must_be :success?
end
end
it "creates contest" do
sign_in users :default
expect {
post :create, contest: { name: "test", admin_id: 1 }
}.must_change "Contest.count"
must_redirect_to contest_path(assigns(:contest))
end
it "shows contest" do
get :show, id: contest
value(response).must_be :success?
end
it "gets edit" do
sign_in users :default
get :edit, id: contest
value(response).must_be :success?
end
it "updates contest" do
sign_in users :default
put :update, id: contest, contest: { name: "bier" }
must_redirect_to contests_path
end
it "destroys contest" do
sign_in users :default
expect {
delete :destroy, id: contest
}.must_change "Contest.count", -1
must_redirect_to contests_path
end
end
Controller below:
class ContestsController < ApplicationController
before_action :set_contest, only: [:show, :edit, :update, :destroy]
# the current user can only edit, update or destroy if the id of the pin matches the id the user is linked with.
before_action :correct_user, only: [:edit, :update, :destroy]
# the user has to authenticate for every action except index and show.
before_action :authenticate_user!, except: [:index, :show]
respond_to :html
def index
#title = t('contests.index.title')
set_meta_tags keywords: %w[leaderboard contest win],
description: "View all the #{Settings.appname} leaderboards now!"
#contests = Contest.all
respond_with(#contests)
end
def show
#title = t('contests.show.title')
#set_meta_tags keywords: %w[],
#description: ""
respond_with(#contest)
end
def new
#title = t('contests.new.title')
#set_meta_tags keywords: %w[],
#description: ""
#contest = current_user.contests.new
respond_with(#contest)
end
def edit
#title = t('contests.edit.title')
#set_meta_tags keywords: %w[],
#description: ""
end
def create
#title = t('contests.create.title')
#set_meta_tags keywords: %w[],
#description: ""
#contest = current_user.contests.new(contest_params)
#contest.admin_id = current_user.id
#contest.save
respond_with(#contest)
end
def update
#title = t('contests.update.title')
#set_meta_tags keywords: %w[],
#description: ""
#contest.update(contest_params)
respond_with(#contest)
end
def destroy
#title = t('contests.destroy.title')
#set_meta_tags keywords: %w[],
#description: ""
#contest.destroy
respond_with(#contest)
end
private
def set_contest
#contest = Contest.find(params[:id])
end
def contest_params
params.require(:contest).permit(:name, :description)
end
def correct_user
#contest = current_user.contests.find_by(id: params[:id])
redirect_to contests_path, notice: t('controller.correct_user') if #contest.nil?
end
end
model contest
has_many :submissions
has_many :users, through: :submissions
belongs_to :admin, class_name: 'User', foreign_key: 'admin_id'
model user
has_many :submissions
has_many :contests, through: :submissions
has_one :contest
It seemed that I do not had my fixture relations set up properly. I have a has many through submissions relations and my fixtures did not have it. Now it is working.
I have following methods in my contest model.
class Contest < ActiveRecord::Base
has_many :submissions
has_many :users, through: :submissions
validates_presence_of :name, :admin_id
acts_as_votable
def admin_name
User.find_by_id(self.admin_id).username
end
def tonnage
self.submissions.sum(:tonnage)
end
def contest_type_tr
I18n.t("contests.contest_type")[self.contest_type]
end
def contest_short_descr
I18n.t("contests.contest_short_descr")[self.contest_type]
end
end
When doing a test for the contest controller I get the following error:
ActionView::Template::Error: undefined method `username' for nil:NilClass
Why is this and how can I fix it?
My specs (minitest) are available below.
require "test_helper"
describe ContestsController do
let(:user) { users :default }
let(:contest) { contests :one }
it "gets index" do
get :index
value(response).must_be :success?
value(assigns(:contests)).wont_be :nil?
end
it "gets new" do
get :new
value(response).must_be :success?
end
it "creates contest" do
expect {
post :create, contest: { }
}.must_change "Contest.count"
must_redirect_to contest_path(assigns(:contest))
end
it "shows contest" do
get :show, id: contest
value(response).must_be :success?
end
it "gets edit" do
get :edit, id: contest
value(response).must_be :success?
end
it "updates contest" do
put :update, id: contest, contest: { }
must_redirect_to contest_path(assigns(:contest))
end
it "destroys contest" do
expect {
delete :destroy, id: contest
}.must_change "Contest.count", -1
must_redirect_to contests_path
end
end
I have a comments_controller that uses inherited_resources and deals with this models:Comment (belongs_to Shop and belongs_to User), and Shop (belongs_to User).
Rails 4.1.1 and Inherited_resources v is 1.5.0.
Routes are:
resources :shop do
resources :comments, only: [:create, :destroy]
end
However, the below code doesn't work:
class CommentsController < InheritedResources::Base
before_filter :authenticate_user!
nested_belongs_to :user, :shop
actions :create, :destroy
def create
#comment = build_resource
#comment.shop = Shop.find(params[:hotel_id])
#comment.user = current_user
create!
end
def destroy
#hotel = Shop.find(params[:hotel_id])
#comment = Comment.find(params[:id])
#comment.user = current_user
destroy!
end
private
def permitted_params
params.permit(:comment => [:content])
end
Rspec that test creation/deletion of comments tell me Couldn't find User without an ID.
Thanks for any help.
UPD One of the failing tests:
let(:user) { FactoryGirl.create(:user) }
let(:shop) { FactoryGirl.create(:shop, user: user) }
describe "comment creation" do
before { visit shop_path(shop) }
describe "with invalid information" do
it "should not create a comment" do
expect { click_button "Post a comment" }.not_to change(Comment, :count)
end
end
From your routes, it looks like you want to deal with Comments belonging to a Shop. In this case, you don't need nested_belongs_to, instead change it to belongs_to :shop in your controller and that will take care of it. And add another line belongs_to :user separately.
So, your controller will look like this:
class CommentsController < InheritedResources::Base
before_filter :authenticate_user!
belongs_to :shop
belongs_to :user
actions :create, :destroy
.
.
.
end
I spent most of the day trying to root out a problem with a controller spec, and the current workaround seems unacceptable to me. Any take on why this works? ... and what I should do instead.
Given a simple hierarchy as follows, and the following ability.rb, the properties_controller_spec.rb does not allow the spec below to pass without the line saying:
ability = Ability.new(subject.current_user)
Can you tell me why this would be?
Thanks!
Models:
class Account < ActiveRecord::Base
has_many :properties, :dependent => :nullify
end
class Property < ActiveRecord::Base
belongs_to :account
end
class User < Refinery::Core::BaseModel #for RefineryCMS integration
belongs_to :account
end
Ability.rb:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.has_role? :user
can [:read, :create, :update, :destroy], Property, account_id: user.account_id
else
can [:show], Property
end
end
end
properties_contoller_spec.rb:
require 'spec_helper'
describe PropertiesController do
def valid_attributes
describe "Authenticated as Property user" do
describe "PUT update" do
describe "with invalid params" do
it "re-renders the 'edit' template" do
property = FactoryGirl.create(:property, account: property_user.account)
# Trigger the behavior that occurs when invalid params are submitted
Property.any_instance.stub(:save).and_return(false)
ability = Ability.new(subject.current_user) # seriously?
put :update, {:id => property.to_param, :property => { }}, {}
response.should render_template("edit")
end
end
end
end
end
Arg! Found it myself.
Here it is:
config.include Devise::TestHelpers, :type => :controller
Following is the code to sign in the property_user, as directed by the Devise docs. (The locals in question are created in a global_variables.rb that is included. These are used all over the place.)
def signed_in_as_a_property_user
property_user.add_role "User"
sign_in property_user
end
def sign_in_as_a_property_user
property_user.add_role 'User'
post_via_redirect user_session_path,
'user[email]' => property_user.email,
'user[password]' => property_user.password
end