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
Related
I want to test comments controller, action create, but I don't know how what's wrong with it. Test are not save comment
comments_controller.rb is work in my projetc, i can see all comments by rails console (as like Comments.all. So that valid:
class CommentsController < ApplicationController
before_action :authenticate_user!, only:[:create,:vote]
before_action :show, only:[:show,:vote]
respond_to :js, :json, :html
def create
#comment = Comment.new(comment_params)
#comment.user_id = current_user.id
if #comment.save
redirect_to post_path(#comment.post.id)
else
redirect_to root_path
end
end
private
def comment_params
params.require(:comment).permit(:comment, :post_id)
end
end
comments_controller_spec.rb is here. It seems like that i send bad params
require 'rails_helper'
RSpec.describe CommentsController, type: :controller do
let(:user) {create :user}
let(:params) { {user_id: user} }
before {sign_in user}
describe '#create' do
let(:post) {create :post}
let(:params) do
{
user_id: user.id,
post_id: post.id,
comment: attributes_for(:comment)
}
end
subject {process :create, method: :post, params: params}
it 'create comment' do
expect{subject}.to change {Comment.count}.by(1)
# is_expected.to redirect_to(user_post_path(assigns(:user), assigns(:post)))
end
end
end
factory comments.rb is here:
FactoryBot.define do
factory :comment do
association :post
association :user
user_id { FFaker::Internet.email }
comment { FFaker::Lorem.sentence }
end
end
I need to create RSpec testing for the following ruby code and seem to run into issues every time I try. I would love an example or two of RSpec tests that could be created for the following code/methods which are in my controller:
def edit
#movie = Movie.find params[:id]
end
def update
#movie = Movie.find params[:id]
#movie.update_attributes!(movie_params)
flash[:notice] = "#{#movie.title} was successfully updated."
redirect_to movie_path(#movie)
end
def destroy
#movie = Movie.find(params[:id])
#movie.destroy
flash[:notice] = "Movie '#{#movie.title}' deleted."
redirect_to movies_path
end
def find_with_same_director
#movie = Movie.find(params[:id])
#movies, check_info = Movie.find_with_same_director(params[:id])
if check_info
flash[:notice] = "'#{#movie.title}' has no director info"
redirect_to movies_path
end
end
I have this so far:
RSpec.describe MoviesController, type: :controller do
it 'should get all movies in the database' do
get :index
expect(response).to render_template :index
expect(assigns[:movies]).to eq(Movie.all)
end
describe 'find_with_same_director' do
it 'should call the find_with_same_director model method' do
expect(Movie).to receive(:find_with_same_director).with(no_args)
get :find_with_same_director, id: movie.id
end
end
end
but it is not covering it correctly. Any help would be appreciated, thanks.
RSpec.describe MoviesController, type: :controller do
it 'should get all movies in the database' do
get :index
expect(response).to render_template :index
expect(assigns[:movies]).to eq(Movie.all)
end
it 'should update movie in the database' do
Factory.girl.create(:movie)
before_movie_count = Movie.count
put :update, id: movie.id
expect(assigns[:movies]).to eq(...) # whatever data is passed
expect(Movie.count).to eq(before_ml_count)
expect(flash[:snack_class]).to eq(:notice.to_s)
expect(flash[:snack_message]).to ends_with('was successfully updated')
expect(response).to have_http_status(:redirect)
end
it 'should delete movie in the database' do
Factory.girl.create(:movie)
before_movie_count = Movie.count
delete :update, id: movie.id
expect(Movie.count).to eq(before_ml_count - 1)
expect(flash[:snack_class]).to eq(:notice.to_s)
expect(flash[:snack_message]).to ends_with('deleted')
expect(response).to have_http_status(:redirect)
end
describe 'find_with_same_director' do
it 'should call the find_with_same_director model method' do
expect(Movie).to receive(:find_with_same_director).with(no_args)
get :find_with_same_director, id: movie.id
end
end
end
#### in factory movie.rb
FactoryGirl.define do
factory :movie do
# assign data to movie attributes here.
end
end
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 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.
I'm building a project to learn Rails and testing, and struggling to troubleshoot errors in an RSpec test of a controller that directs a nested resource. My code works as expected in a browser. I believe the problem relates to my test set-up and the associations of FactoryGirl objects. I need help troubleshooting and fixing the controller spec.
Here's the cardio_exercises_controller.rb
class CardioExercisesController < ApplicationController
# :get_member is defined in the private method at the bottom of this file,
# and takes the member_id provided by the routing and
#converts it to a #member object.
before_action :get_member
# GET member/1/cardio_exercises
# GET member/1/cardio_exercises.json
def index
#cardio_exercises = #member.cardio_exercises
end
# GET member/1/cardio_exercises/1
# GET member/1/cardio_exercises/1.json
def show
cardio_exercise = #member.cardio_exercises.find(params[:id])
end
# GET member/1/cardio_exercises/new
def new
#member = Member.find(params[:member_id])
#cardio_exercise = #member.cardio_exercises.build
end
# GET member/1/cardio_exercises/1/edit
def edit
#cardio_exercise = #member.cardio_exercises.find(params[:id])
end
# POST member/1/cardio_exercises
# POST member/1/cardio_exercises.json
def create
#cardio_exercise = #member.cardio_exercises.build(cardio_exercise_params)
if #cardio_exercise.save
flash[:success] = "Cardio exercise was successfully created."
redirect_to member_cardio_exercises_path(#member)
else
render 'new'
end
end
# PATCH/PUT member/1/cardio_exercises/1
# PATCH/PUT member/1/cardio_exercises/1.json
def update
#cardio_exercise = #member.cardio_exercises.find(params[:id])
if #cardio_exercise.update(cardio_exercise_params)
flash[:success] = "Cardio exercise was successfully updated."
redirect_to member_cardio_exercises_path(#member)
else
render 'edit'
end
end
# DELETE member/1/cardio_exercises/1
# DELETE member/1/cardio_exercises/1.json
def destroy
#cardio_exercise = #member.cardio_exercises.find(params[:id])
#cardio_exercise.destroy
respond_to do |format|
format.html { redirect_to (member_cardio_exercises_path(#member)), notice: 'Cardio exercise was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# The get_member action converts the member_id given by the routing
# into an #member object, for use here and in the view.
def get_member
#member = Member.find(params[:member_id])
end
def cardio_exercise_params
params.require(:cardio_exercise).permit(:title, :duration, :calories_burned, :date, :member_id)
end
end
Here's the cardio_exercises_controller_spec.rb
require 'rails_helper'
RSpec.describe CardioExercisesController, :type => :controller do
before :each do
#member = FactoryGirl.create(:member)
#cardio_exercise = FactoryGirl.create(:cardio_exercise)
#cardio_exercise_attributes = FactoryGirl.attributes_for(:cardio_exercise, :member_id => #member)
end
describe "GET index" do
it "assigns all cardio_exercises as #member.cardio_exercises" do
get :index, { :member_id => #member }
expect(assigns(:cardio_exercises)).to eq(#member.cardio_exercises)
end
end
describe "GET show" do
it "assigns the requested cardio_exercise as #member.cardio_exercise" do
get :show, { :member_id => #member, :id => #cardio_exercise }
expect(assigns(:cardio_exercise)).to eq(#member.cardio_exercise)
end
end
describe "GET new" do
it "assigns a new cardio_exercise as #member.cardio_exercise" do
get :new, { :member_id => #member }
expect(assigns(:cardio_exercise)).to be_a_new(CardioExercise)
end
end
describe "GET edit" do
it "assigns the requested cardio_exercise as #member.cardio_exercise" do
end
end
describe "POST create" do
describe "with valid params" do
it "creates a new CardioExercise" do
expect {
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
}.to change(CardioExercise, :count).by(1)
end
it "assigns a newly created cardio_exercise as #cardio_exercise" do
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
expect(assigns(:cardio_exercise)).to be_a(CardioExercise)
expect(assigns(:cardio_exercise)).to be_persisted
end
it "redirects to the created cardio_exercise" do
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
expect(response).to redirect_to(CardioExercise.last)
end
end
end
describe "PUT update" do
describe "with invalid params" do
xit "updates the requested cardio_exercise" do
#put :update, { id: #member.id, member_id: cardio_exercise: #cardio_exercise.id }
end
xit "assigns the requested cardio_exercise as #member.cardio_exercise" do
end
xit "redirects to the cardio_exercise" do
end
end
describe "with invalid params" do
xit "assigns the cardio_exercise as #member.cardio_exercise" do
end
xit "re-renders the 'edit' template" do
expect(response).to render_template("edit")
end
end
end
describe "DELETE destroy" do
it "destroys the requested cardio_exercise" do
expect {
delete :destroy, { :member_id => #member, :id => #cardio_exercise }
}.to change(CardioExercise, :count).by(-1)
end
it "redirects to the cardio_exercises list" do
delete :destroy, { :member_id => #member, :id => #cardio_exercise }
expect(response).to redirect_to(member_cardio_exercises_url)
end
end
end
These are the relevant factories:
FactoryGirl.define do
factory :cardio_exercise do
title "My cardio exercise"
duration 30
calories_burned 300
date "2014-11-15"
association :member
end
end
FactoryGirl.define do
factory :member do
first_name {Faker::Name.first_name}
last_name {Faker::Name.last_name}
age 21
height 75
weight 195
goal "fffff" * 5
start_date "2014-11-15"
end
end
routes.rb contains:
resources :members do
resources :cardio_exercises
end
members.rb contains:
has_many :cardio_exercises, :dependent => :destroy
cardio_exercises.rb contains:
belongs_to :member
Rspec Failures/Errors:
1) CardioExercisesController GET show assigns the requested cardio_exercise as #member.cardio_exercise
Failure/Error: get :show, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=25 [WHERE "cardio_exercises"."member_id" = $1]
2) CardioExercisesController POST create with valid params redirects to the created cardio_exercise
Failure/Error: expect(response).to redirect_to(CardioExercise.last)
NoMethodError:
undefined method `cardio_exercise_url' for #<CardioExercisesController:0x00000008bba960>
3) CardioExercisesController DELETE destroy destroys the requested cardio_exercise
Failure/Error: delete :destroy, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=34 [WHERE "cardio_exercises"."member_id" = $1]
4) CardioExercisesController DELETE destroy redirects to the cardio_exercises list
Failure/Error: delete :destroy, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=35 [WHERE "cardio_exercises"."member_id" = $1]
I think the Record Not Found errors indicate a problem with the associations between the member and cardio exercise models. The controller isn't finding the cardio exercise by its id. What have I missed in setting things up for RSpec? What's the best way to fix the set up?
The Undefined method error appears to be caused by my calling the last method on CardioExercise. I'm posting to the create method in the example. CardioExercise is the class. Can someone explain why that call triggers the error, and how to fix it?
I appreciate any help!
You create two independent objects/records: member and cardio_exercise.
You should pass #member to factory cardio_exercise to connect them.
#member = FactoryGirl.create(:member)
#cardio_exercise = FactoryGirl.create(:cardio_exercise, member: #member)
PS When you create cardio_exercise without setup member factory creates new record in table members and assign cardio_exercise with this new record
UPDATE
about "GET index"
You create #member without any associated cardio_exercises. You added them later and object #member knows nothing about it. You should reload object to fetch data from DB
expect(assigns(:cardio_exercises)).to eq(#member.reload.cardio_exercises)
and sometimes I convert relations to array and sort result to avoid failing tests when order is different