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
Related
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.
I inspired myself with the following link, http://railscasts.com/episodes/163-self-referential-association, but the rspec testing is not coming easy.
user model:
class User < ActiveRecord::Base
# Associations
has_many :followerships
has_many :followers, :through => :followerships
has_many :inverse_followerships, :class_name => "Followership", :foreign_key => "follower_id"
has_many :inverse_followers, :through => :inverse_followerships, :source => :user
end
followership model:
class Followership < ActiveRecord::Base
belongs_to :user
belongs_to :follower, :class_name => "User"
end
followerhip factory:
FactoryGirl.define do
factory :followership do
user_id 1
follower_id 1
end
end
followerships controller:
class FollowershipsController < InheritedResources::Base
def create
#followership = current_user.followerships.build(:follower_id => params[:follower_id])
if #followership.save
flash[:notice] = "Following."
redirect_to root_url
else
flash[:error] = "Unable to follow."
redirect_to root_url
end
end
def destroy
#followership = current_user.followerships.find(params[:id])
#followership.destroy
flash[:notice] = "Removed followership."
redirect_to current_user
end
end
folowerships controller spec (this is all wrong):
require 'rails_helper'
describe FollowershipsController do
let!(:followership) { create(:followership) }
let!(:follower) { followership.follower }
let!(:user) { create(:user) }
before do
sign_in :user, user
end
describe "#create" do
it "saves the followership" do
post :create, followership: { follower_id: follower }
expect(response).to redirect_to(root_path)
expect(assigns(:followership).followership.followers).to eq(user)
expect(flash[:notice]).to eq("Following.")
end
it "fails to save followership" do
expect(post :create, followership: { follower_id: follower }).to redirect_to(root_path)
expect(flash[:notice]).to eq("Unable to follow.")
end
end
describe "#destroy" do
it "deletes the followership" do
expect {
delete :destroy, id: follower
}.to change(Followership, :count).by(-1)
expect(flash[:notice]).to eq("Removed followership.")
end
end
end
Error from followerships controller Rspec
FollowershipsController
#destroy
deletes the followership (FAILED - 1)
#create
saves the followership (FAILED - 2)
fails to save followership (FAILED - 3)
Failures:
1) FollowershipsController#destroy deletes the followership
Failure/Error: delete :destroy, id: follower
ActionController::UrlGenerationError:
No route matches {:action=>"destroy", :controller=>"followerships", :id=>nil}
2) FollowershipsController#create saves the followership
Failure/Error: expect(assigns(:followership).followership.followers).to eq(user)
NoMethodError:
undefined method `followership' for #<Followership:0x00000109f69780>
3) FollowershipsController#create fails to save followership
Failure/Error: expect(flash[:notice]).to eq("Unable to follow.")
expected: "Unable to follow."
got: "Following."
(compared using ==)
Thanks for the help :)
The let command uses lazy evaluation, so these records are not actually created until called. Use the let! syntax to ensure they're created before your tests run:
let!(:followership) { create(:followership) }
let!(:follower) { followership.follower }
let!(:user) { create(:user) }
Make sure your validations also only allow creation of a following if it doesn't already exist for that pair of users:
class Followership < ActiveRecord::Base
validates_uniqueness_of :user_id, scope: :follower_id
Also, it's not guaranteed that the follower/followership relationships will belong to user since user doesn't necessarily have an id of 1.
Finally, assigns is a method, so the syntax should be assigns(:followership) not assigns[:followership]
I'm currently working with a small team on an open source Rails 4 chess application, and I'm trying to test out various possible piece moves in RSpec (including special cases such as en passant and castling). A senior web developer suggested that I use a separate table to keep track of the moves taken in each game of chess. After taking him up on his suggestion, I'm having trouble testing out valid moves, as shown in the error message below. I suspect that it might be a problem with my associations, but the teammates that I was able to talk to about this were unsure about the cause.
Any help would be greatly appreciated.
Error message:
Failures:
1) PiecesController Action: pieces#update should create a move when a move is valid
Failure/Error: #current_game ||= current_piece.game
NoMethodError:
undefined method `game' for nil:NilClass
# ./app/controllers/pieces_controller.rb:36:in `current_game'
# ./app/controllers/pieces_controller.rb:40:in `require_authorized_for_current_game'
# ./spec/controllers/pieces_controller_spec.rb:12:in `block (3 levels) in <top (required)>'
The test:
RSpec.describe PiecesController, type: :controller do
describe "Action: pieces#update" do
it "should create a move when a move is valid" do
user_sign_in
game = FactoryGirl.create(:game)
# Test a white pawn's movement on its first turn:
piece = FactoryGirl.create(:piece)
move = FactoryGirl.create(:move)
# Why can't I call game.id below?
patch :update, :id => game.id, :pieces => { }
piece_params = { :x_position => piece.x_position, :y_position => piece.y_position, :type => "Pawn" }
if piece.valid_move?(piece_params)
...
end
end
end
private
def user_sign_in
user = FactoryGirl.create(:user)
sign_in user
end
end
Associations:
class Game < ActiveRecord::Base
has_many :pieces
has_many :moves, through: :pieces
belongs_to :white_player, class_name: 'User', foreign_key: :white_player_id
belongs_to :black_player, class_name: 'User', foreign_key: :black_player_id
...
end
class Piece < ActiveRecord::Base
belongs_to :game
has_many :moves
def valid_move?(params)
...
end
...
end
class Move < ActiveRecord::Base
belongs_to :piece
end
Factories:
FactoryGirl.define do
factory :user do
...
end
factory :game do
association :white_player, factory: :user
association :black_player, factory: :user
turn 1
end
factory :piece do
association :game
...
end
# Set up an initially empty move, then adjust the values after checking that a piece can be moved:
factory :move do
association :piece
...
end
end
The controller:
class PiecesController < ApplicationController
before_action :authenticate_user!
before_action :require_authorized_for_current_game, only: [:update]
before_action :require_authorized_for_current_piece, only: [:update]
def update
...
end
...
private
def current_piece
#current_piece ||= Piece.find_by_id(params[:id])
end
...
def piece_params
params.require(:piece).permit(:x_position, :y_position, :type, :captured)
end
def current_game
#current_game ||= current_piece.game
end
def require_authorized_for_current_game
if current_game.white_player != current_user && current_game.black_player != current_user
render text: 'Unauthorized', status: :unauthorized
end
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've got two models:
class Solution < ActiveRecord::Base
belongs_to :user
validates_attachment_presence :software
validates_presence_of :price, :language, :title
validates_uniqueness_of :software_file_name, :scope => :user_id
has_attached_file :software
end
class User < ActiveRecord::Base
acts_as_authentic
validates_presence_of :first_name, :last_name, :primary_phone_number
validates_uniqueness_of :primary_phone_number
has_many :solutions
end
with my routes looking like this:
map.resources :user, :has_many => :solutions
Now I'm trying to test my solutions controllers with the following RSpec test:
describe SolutionsController do
before(:each) do
#user = Factory.build(:user)
#solution = Factory.build(:solution, :user => #user)
end
describe "GET index" do
it "should find all of the solutions owned by a user" do
Solution.should_receive(:find_by_user_id).with(#user.id).and_return(#solutions)
get :index, :id => #user.id
end
end
end
However, this gets me the following error:
ActionController::RoutingError in 'SolutionsController GET index should find all of the solutions owned by a user'
No route matches {:id=>nil, :controller=>"solutions", :action=>"index"}
Can anybody point me to how I can test this, since the index should always be called within the scope of a particular user?
Factory#build builds an instance of the class, but doesn't save it, so it doesn't have an id yet.
So, #user.id is nil because #user has not been saved.
Because #user.id is nil, your route isn't activated.
try using Factory#create instead.
before(:each) do
#user = Factory.create(:user)
#solution = Factory.create(:solution, :user => #user)
end
Looks like your other problem is on this line:
get :index, :id => #user.id
You're trying to make a request to the index method, but you've provided the wrong variable name. When testing SolutionsController id implies a solution id, you need to supply the user id. This should work, or at least move you forward:
get :index, :user_id => #user.id