I have this issue with test my CommentsController:
Failure/Error: redirect_to user_path(#comment.user), notice: 'Your
comment was successfully added!' ActionController::UrlGenerationError:
No route matches {:action=>"show", :controller=>"users", :id=>nil}
missing required keys: [:id]
This is my method in my controller:
def create
if params[:parent_id].to_i > 0
parent = Comment.find_by_id(params[:comment].delete(:parent_id))
#comment = parent.children.build(comment_params)
else
#comment = Comment.new(comment_params)
end
#comment.author_id = current_user.id
if #comment.save
redirect_to user_path(#comment.user), notice: 'Your comment was successfully added!'
else
redirect_to user_path(#comment.user), notice: #comment.errors.full_messages.join
end
end
This is my RSpec:
context "User logged in" do
before :each do
#user = create(:user)
sign_in #user
end
let(:comment) { create(:comment, user: #user, author_id: #user.id) }
let(:comment_child) { create(:comment_child, user: #user, author_id: #user.id, parent_id: comment.id) }
describe "POST #create" do
context "with valid attributes" do
it "saves the new comment object" do
expect{ post :create, comment: attributes_for(:comment), id: #user.id}.to change(Comment, :count).by(1)
end
it "redirect to :show view " do
post :create, comment: attributes_for(:comment), user: #user
expect(response).to redirect_to user_path(comment.user)
end
end
...
end
end
My Comment model:
class Comment < ActiveRecord::Base
belongs_to :user
acts_as_tree order: 'created_at DESC'
VALID_REGEX = /\A^[\w \.\-#:),.!?"']*$\Z/
validates :body, presence: true, length: { in: 2..240}, format: { with: VALID_REGEX }
end
How Can I add user_id to that request? When I change code in my controller redirect_to user_path(#comment.user) to redirect_to user_path(current_user) - test pass. May I redirect_to user in comments controller? Is any posibility to do it right? Thanks for your time.
Basically the error is caused by the fact that the #comment.user is nil.
Lets start fixing it by cleaning up the spec:
context "User logged in" do
# declare lets first.
let(:user) { create(:user) }
let(:comment) { create(:comment, user: user, author: user) }
# use do instead of braces when it does not fit on one line.
let(:comment_child) do
# use `user: user` instead of `user_id: user.id`.
# the latter defeats the whole purpose of the abstraction.
create(:comment_child, user: user, author: user, parent: comment)
end
before { sign_in(user) }
describe "POST #create" do
context "with valid attributes" do
it "saves the new comment object" do
expect do
post :create, comment: attributes_for(:comment)
end.to change(Comment, :count).by(1)
end
it "redirects to the user" do
post :create, comment: attributes_for(:comment)
expect(response).to redirect_to user
end
end
end
end
You should generally avoid using instance vars and instead use lets in most cases. Using a mix just adds to the confusion since its hard to see what is lazy loaded or even instantiated where.
Then we can take care of the implementation:
def create
#comment = current_user.comments.new(comment_params)
if #comment.save
redirect_to #comment.user, notice: 'Your comment was successfully added!'
else
# ...
end
end
private
def comment_params
# note that we don't permit the user_id to be mass assigned
params.require(:comment).permit(:foo, :bar, :parent_id)
end
Basically you can cut a lot of the overcomplication:
Raise an error if there is no authenticated user. With Devise you would do before_action :authenticate_user!.
Get the user from the session - not the params. Your not going to want or need users to comment on the behalf of others.
Wrap params in the comments key.
Use redirect_to #some_model_instance and let rails do its polymorpic routing magic.
Let ActiveRecord throw an error if the user tries to pass a bad parent_id.
Also does your Comment model really need both a user and author relationship? Surely one of them will suffice.
Related
I'm trying to test the 'destroy' action for my nested comments controller.
User model has_many :comments, dependent: :destroy
Movie model has_many :comments, dependent: :destroy
Comments model belongs_to :user and :movie
Here is my comments controller
def create
#comment = #movie.comments.new(comment_params.merge(user: current_user))
if #comment.save
flash[:notice] = 'Comment successfully added'
redirect_to #movie
else
flash.now[:alert] = 'You can only have one comment per movie'
render 'movies/show'
end
end
def destroy
#comment = #movie.comments.find(params[:id])
if #comment.destroy
flash[:notice] = 'Comment successfully deleted'
else
flash[:alert] = 'You are not the author of this comment'
end
redirect_to #movie
end
private
def comment_params
params.require(:comment).permit(:body)
end
def set_movie
#movie = Movie.find(params[:movie_id])
end
Of course there is also before_action :set_movie, only: %i[create destroy] at the top.
Here are my specs, I'm using FactoryBot and all factories works fine in other examples so I think the issue is somewhere else.
describe "DELETE #destroy" do
let(:user) { FactoryBot.create(:user) }
let(:movie) { FactoryBot.create(:movie) }
before do
sign_in(user)
end
it "deletes comment" do
FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params { movie_id: movie.id }
end.to change(Comment, :count).by(-1)
expect(response).to be_successful
expect(response).to have_http_status(:redirect)
end
end
I've got an error ActionController::UrlGenerationError: No route matches {:action=>"destroy", :controller=>"comments", :movie_id=>1}
I think my address in specs destroy action is wrong but how to define it in a good way?
You need to specify id of a comment you want to remove:
it "deletes comment" do
comment = FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params { id: comment.id, movie_id: movie.id }
end.to change(Comment, :count).by(-1)
# ...
end
I want to contribute here with one more approach. Sometimes you have to be sure that you've deleted the exact instance (comment_1, not comment_2).
it "deletes comment" do
comment_1 = FactoryBot.create(:comment, movie: movie, user: user)
comment_2 = FactoryBot.create(:comment, movie: movie, user: user)
delete :destroy, params { id: comment_1.id, movie_id: movie.id }
expect { comment_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
# ...
end
Somehow I was never able to pass an ID, either in params {} or as a direct argument following "delete". Instead, I made it work like so:
it "DELETE will cause an exception" do
delete "/comments/#{comment.id}"
expect { comment.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
In my Rails 4 app I have this update action:
class UsersController < ApplicationController
...
def update
current_email = #user.email
new_email = user_params[:email].downcase
if #user.update_attributes(user_params)
if current_email != new_email
#user.email = current_email
#user.new_email = new_email.downcase
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
redirect_to edit_user_path(#user)
else
render :edit
end
end
...
end
It basically makes sure that a user cannot simply save any new email address. He will have to confirm it first by clicking on a link in an email we send to him.
This works great, however, for some reason I haven't found a way to test it.
The following RSpec test keeps failing no matter what I do:
it "changes the user's new_email attribute" do
#user = FactoryGirl.create(:user, :email => "john#doe.com")
patch :update, :id => #user, :user => FactoryGirl.attributes_for(:user, :email => "new#email.com")
expect(#user.reload.new_email).to eq("new#email.com")
end
#user.new_email is always nil and the test always fails. What am I missing here?
Re-factoring my update action wouldn't be a problem at all. Maybe there's a better way? Thanks for any help.
I would write the spec like so:
let(:user) { FactoryGirl.create(:user, email: "john#doe.com") }
it "changes the user's new_email attribute" do
expect do
patch :update, id: #user, user: FactoryGirl.attributes_for(:user, email: "new#email.com")
user.reload
end.to change(user, :new_email).from("john#doe.com").to("new#email.com")
end
When it comes to the controller action itself the problem is that the new_email property is never saved to the database, besides that its kind of a mess. You can clean it up by using ActiveRecord::Dirty which tracks attribute changes in the model:
class User < ApplicationRecord
# updates user with attrs but moves a new email to the `new_email`
# column instead
def update_with_email(attrs, &block)
update(attrs) do |record|
if record.email_changed?
record.new_email = record.email.downcase
record.restore_attribute!(:email)
end
# keeps the method signature the same as the normal update
yield record if block_given?
end
end
end
Putting this business logic in the model also lets you test it separatly:
RSpec.describe User, type: :model do
describe "#update_with_email" do
let(:user) { FactoryGirl.create(:user) }
it "does not change the email attribute" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to_not change(user, :email)
end
it "updates the new_email" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to change(user, :new_email).to('xxx#example.com')
end
end
end
This lets you keep the controller nice and skinny:
def update
if #user.update_with_email(user_params)
if #user.new_email_changed?
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
# You probably want to redirect the user away from the form instead.
redirect_to edit_user_path(#user)
else
render :edit
end
end
With which matcher and how can I test if the #post_comment and #post_comment.user is properly assigned?
expect(assigns(:post_comment)).to be_a_new(PostComment) is not working here.
UPDATE:
With the following setup I also get the following error. What should I change to be able to test the invalid attrs?
Posts::PostCommentsController when user is logged in POST create with invalid attributes doesn't save the new product in the db
Failure/Error: #post_comment.save!
ActiveRecord::RecordInvalid:
Validation failed: Body can't be blank
IF I delete #post_comment.save! then I get
Posts::PostCommentsController when user is logged in POST create with invalid attributes doesn't save the new product in the db
Failure/Error: <span class="post-comment-updated"><%= local_time_ago(post_comment.updated_at) %></span>
ActionView::Template::Error:
undefined method `to_time' for nil:NilClass
post_comments_controller
def create
#post = Post.find(params[:post_id])
#post_comment = #post.post_comments.build(post_comment_params)
authorize #post_comment
#post_comment.user = current_user
#post_comment.save!
if #post_comment.save
#post.send_post_comment_creation_notification(#post_comment)
#post_comment_reply = PostCommentReply.new
respond_to do |format|
format.html { redirect_to posts_path, notice: "Comment saved!" }
format.js
end
end
end
post_comments_controller_spec.rb
describe "POST create" do
let!(:profile) { create(:profile, user: #user) }
let!(:post_instance) { create(:post, user: #user) }
context "with valid attributes" do
subject(:create_action) { xhr :post, :create, post_id: post_instance.id, post_comment: attributes_for(:post_comment, post_id: post_instance.id, user: #user) }
it "saves the new task in the db" do
expect{ create_action }.to change{ PostComment.count }.by(1)
end
it "assigns instance variables" do
create_action
expect(assigns(:post)).to eq(post_instance)
#########How to test these two?
#expect(assigns(:post_comment)).to be_a_new(PostComment)
#expect(assigns(:post_comment.user)).to eq(#user)
expect(assigns(:post_comment_reply)).to be_a_new(PostCommentReply)
end
it "assigns all the instance variables"
it "responds with success" do
create_action
expect(response).to have_http_status(200)
end
end
context "with invalid attributes" do
subject(:create_action) { xhr :post, :create, post_id: post_instance.id, post_comment: attributes_for(:post_comment, post_id: post_instance.id, user: #user, body: "") }
it "doesn't save the new product in the db" do
expect{ create_action }.to_not change{ PostComment.count }
end
end
end
How to test these two?
expect(assigns(:post_comment)).to be_a_new(PostComment)
expect(assigns(:post_comment.user)).to eq(#user)
I believe you shoudl test not a new record, but a record of a class, and persisted record:
expect(assigns(:post_comment)).to be_a(PostComment)
expect(assigns(:post_comment)).to be_presisted
expect(assigns(:post_comment.user)).to eq(#user)
Excessive code.
#post_comment.save!
if #post_comment.save
You shall to keep only the single record of that, I believe it is enough save with exception:
#post_comment.save!
So other part code you can pick out of if block. Exception from save! you shall to trap with rescue_from.
I have an article model which has many comments and the comment belongs to one article. this is my create method for comments_controller.rb:
def create
#comment = Comment.new(comment_params)
#comment.article_id = params[:article_id]
#comment.save
redirect_to article_path(#comment.article)
end
I want to know what's the best approach to test this action with rspec. and I want to know testing methods for association in controller at all.
thank you experts.
You can access your comment object within your tests using assigns method:
describe CommentsController, type: :controller
let(:comment_params) {{ <correct params goes here>}}
let(:article_id) { (1..100).sample }
let(:create!) { post :create, comment: comment_params, article_id: article_id }
it "creates new comment" do
expect { create! }.to change { Comment.count }.by 1
end
it "assigns given comment to correct article"
create!
expect(assigns(:comment).article_id).to eq params[:article_id]
end
end
The above is just a guideline, you will need to modify it depending on your exact requirements.
I suggest this codes.
This code is using FactoryGirl.
factory_girl is a fixtures replacement with a straightforward definition syntax... https://github.com/thoughtbot/factory_girl
Please add gem 'factory_girl_rails' to Gemfile.
def create
#comment = Comment.new(comment_params)
#comment.article_id = params[:article_id]
if #comment.save
redirect_to article_path(#comment.article)
else
redirect_to root_path, notice: "Comment successfully created" # or you want to redirect path
end
end
describe "POST #create" do
let(:article_id) { (1..100).sample }
context 'when creation in' do
it 'creates a new comment' do
expect { post :create, comment: attributes_for(:comment), article_id: article_id }.to change {
Comment.count
}.from(0).to(1)
end
it 'returns same article_id' do
post :create, comment: attributes_for(:comment), article_id
expect(assigns(:comment).article_id).to eq(article_id)
end
end
context 'when successed in' do
before { post :create, comment: attributes_for(:comment), article_id }
it 'redirects article path' do
expect(response).to redirect_to(Comment.last.article)
end
end
context 'when unsuccessed in' do
before { post :create, comment: attributes_for(:comment), article_id }
it 'does not redirect article path' do
expect(response).to redirect_to(root_path)
end
end
end
uhh, I am not English native speaker. so If it's sentence is not natural, please modify sentences. :-(
I am trying to test a controller create method in a rails app using RSpec as shown below:
def create
#user = User.new(user_params)
if #user.save
redirect_to user_path(#user.id)
else
render new_user_path
flash[:error] = "User not saved"
end
end
However if i stub out .new to prevent the test from using Active Record and the User model by forcing it to return true the id of the #user is not set by .save as normal so I cannot test for it redirecting to user_path(#user.id) as #user.id is nil
Here is my initial test for RSpec:
it "creates a user and redirects" do
expect_any_instance_of(User).to receive(:save).and_return(true)
post :create, { user: {name: "John", username: "Johnny98", email: "johnny98#example.com"} }
expect(assigns(:user).name).to eq("John")
expect(response).to redirect_to user_path(assigns(:user))
end
How should I test for this redirect in RSpec.
You should use mocks - https://www.relishapp.com/rspec/rspec-mocks/docs.
user = double("user", id: 1, save: true)
Then you should mock you method with double you've just created
expect(User).to receive(:new).and_return(user)
And then test redirect.
expect(response).to redirect_to user_path(user)
I hope this will help.
I would do it in this way:
it 'should redirect to a user if save returned true' do
#user_instance = double
#user_id = double
allow(User).to receive(:new).and_return(#user_instance)
allow(#user_instance).to receive(:save).and_return(true)
allow(#user_instance).to receive(:id).and_return(#user_id)
post :create, {:user => valid_attributes}
expect(response).to redirect_to(user_path(#user_id))
end