How do I test association in rails controller with rspec? - ruby-on-rails

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. :-(

Related

rspec controller spec matchers

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.

RSpec controller: redirect_to other controller

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.

How to create DRYer Rspec with Factory Girl

I am building a simple blog app in order to learn BDD/TDD with RSpec and Factory Girl. Through this process, I continue to run into 'Failures' but I believe they have more to do with how I am using Factory Girl than anything.
As you'll see below, in order to get my specs to pass, I'm having a hard time keeping my test DRY - there must be something I am misunderstanding. You'll notice, I'm not using Factory Girl to it's full potential and at times, skipping it altogether. I find that I commonly run into problems when using functions such as get :create, get :show, or put :update within the spec.
I am currently stuck on the #PUT update spec that should simply test the assignment of the #post variable. I have tried multiple types of this spec that I found online, yet none seem to work - hence, is it Factory Girl? Maybe the specs I'm finding online are outdated Rspec versions?
I'm using:
Rspec 3.1.7
Rails 4.1.6
posts_controller_spec.rb
require 'rails_helper'
require 'shoulda-matchers'
RSpec.describe PostsController, :type => :controller do
describe "#GET index" do
it 'renders the index template' do
get :index
expect(response).to be_success
end
it "assigns all posts as #posts" do
post = Post.create(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :index
expect(assigns(:posts)).to eq([post])
end
end
describe '#GET show' do
it 'assigns the request post to #post' do
post = Post.create!(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :show, id: post.id
expect(assigns(:post)).to eq(post)
end
end
describe '#GET create' do
context 'with valid attributes' do
before :each do
post :create, post: attributes_for(:post)
end
it 'creates the post' do
expect(Post.count).to eq(1)
expect(flash[:notice]).to eq('Your post has been saved!')
end
it 'assigns a newly created post as #post' do
expect(assigns(:post)).to be_a(Post)
expect(assigns(:post)).to be_persisted
end
it 'redirects to the "show" action for the new post' do
expect(response).to redirect_to Post.first
end
end
context 'with invalid attributes' do
before :each do
post :create, post: attributes_for(:post, title: 'ha')
end
it 'fails to create a post' do
expect(Post.count).to_not eq(1)
expect(flash[:notice]).to eq('There was an error saving your post.')
end
it 'redirects to the "new" action' do
expect(response).to redirect_to new_post_path
end
end
end
describe '#GET edit' do
it 'assigns the request post to #post' do
post = Post.create!(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :edit, id: post.id
expect(assigns(:post)).to eq(post)
end
end
describe '#PUT update' do
context 'with success' do
before :each do
post :create, post: attributes_for(:post)
end
it 'assigns the post to #post' do
put :update, id: post.id
expect(assigns(:post)).to eq(post)
end
end
end
end
posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all.order('created_at DESC')
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
if #post.save
flash[:notice] = "Your post has been saved!"
redirect_to #post
else
flash[:notice] = "There was an error saving your post."
redirect_to new_post_path
end
end
def show
#post = Post.find(params[:id])
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
# if #post.update(params[:post].permit(:title, :body))
# flash[:notice] = "Your post is updated!"
# redirect_to #post
# else
# flash[:notice] = "There was an error updating your post."
# render :edit
# end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
factories/post.rb
FactoryGirl.define do
factory :post do
title 'First title ever'
body 'Forage paleo aesthetic food truck. Bespoke gastropub pork belly, tattooed readymade chambray keffiyeh Truffaut ennui trust fund you probably haven\'t heard of them tousled.'
end
end
Current Failure:
Failures:
1) PostsController#PUT update with success assigns the post to #post
Failure/Error: put :update, id: post.id
ArgumentError:
wrong number of arguments (0 for 1+)
# ./spec/controllers/posts_controller_spec.rb:86:in `block (4 levels) in <top (required)>'
Finished in 0.19137 seconds (files took 1.17 seconds to load)
17 examples, 1 failure
You could definitely leverage factories here.
The factory you've created is actually fine too.
Instead of doing:
post = Post.create(title: 'Charlie boy', body: 'Bow wow wow ruff')
Do this: post = FactoryGirl.create(:post)
You can get ever more DRY if you do this:
# in spec/rails_helper.rb
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
This will allow you do this in your spec: post = create(:post)
Regarding your PUT test, try this from a previous SO answer:
describe '#PUT update' do
let(:attr) do
{ :title => 'new title', :content => 'new content' }
end
context 'with success' do
before :each do
#post = FactoryGirl.create(:post)
end
it 'assigns the post to #post' do
put :update, :id => #post.id, :post => attr
#post.reload
expect(assigns(:post)).to eq(post)
end
end
end
Edit:
Also, don't be afraid of moving things in to a before :each do if you need to. They are great at keeping things DRY
The immediate reason why your spec is failing is because you can only call on the controller once per test, and for update you're calling it twice: in the before-action, you are calling create... and then in the main part of the update test you are calling update... controller specs don't like that.
In order to get the existing spec working, you would need to replace the post :create, post: attributes_for(:post) line in the before-action with just creating a post or (as mentioned already) using factory girl to create a post - rather than trying to do it by calling the controller to do it.

'undefined method' post with Rails and Rspec

Got stuck with:
' undefined method `post' for #<Class:0x000001058c0f68> (NoMethodError)'
on testing controller create action.
I'm using Rails 4, rpsec, and Factory Girl
Controller:
def create
#post = Post.new(post_params)
#post.user_id = current_user.id
if #post.save
flash[:success] = "Yay! Post created!"
redirect_to root_path
else
# flash[:error] = #post.errors.full_messages
render 'new'
end
end
Test:
describe '#create' do
post 'create', FactoryGirl.attributes_for(:post, user: #user)
response.should be_successful
end
I think post method is accessible inside it method block:
describe 'create' do
it 'should be successful' do
post :create, FactoryGirl.attributes_for(:post, user: #user)
response.should be_success
end
end
BTW I think you need to test for redirect, not success status.
Sorry for being off-topic but I just want to give you some advice.
Consider following best practices and use RSpec's expect syntax instead of should. Read more about why the should syntax is a bad idea here: http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
This is how I would rewrite your example:
describe 'create' do
it 'responds with 201' do
post :create, attributes_for(:post, user: #user)
expect(response.status).to eq(201)
end
end
In the example I'm using FactoryGirl's short syntax method attributes_for instead of FactoryGirl.attributes_for, it saves a few bytes. Here's how to make the short methods available (in spec/test_helper.rb):
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
I'm testing for the status code 201 which Rails will return by default for a successful create action (redirect should be 3xx).This makes the test more specific.
Hope it's any help for writing better specs.
The issue comes from the fact that post should be used inside an it statement. I usually test my controllers like this:
describe 'POST "create"' do
let(:user) { User.new }
let(:params) { FactoryGirl.attributes_for(:post, user: user) }
let(:action) { post :create, params }
let!(:post) { Post.new }
before do
Post.should_receive(:new).and_return(post)
end
context 'on success' do
before do
post.should_receive(:save).and_return(true)
end
it 'renders success' do
action
expect(response).to be_success
end
it 'redirects' do
action
expect(response).to be_redirected
end
it 'sets flash message' do
action
expect(flash[:success]).to_not be_empty
end
end
context 'on failure' do
before do
post.should_receive(:save).and_return(false)
end
it 'renders new' do
action
expect(response).to render_template(:new)
end
end
end

testing "create" method in ruby with rspec

I have written this controller code in Ruby on Rails
class PostsController < ApplicationController
before_filter :authenticate_user!
def index
#posts = Post.all(:order => "created_at DESC")
respond_to do |format|
format.html
end
end
def create
#post = Post.create(:message => params[:message])
respond_to do |format|
if #post.save
format.html { redirect_to posts_path }
format.js
else
flash[:notice] = "Message failed to save."
format.html { redirect_to posts_path }
end
end
end
end
and corresponding to this I have written the following test case :-
require 'spec_helper'
describe PostsController do
describe "GET 'index'" do
it "returns http success" do
get 'index'
response.should be_success
end
end
describe "#create" do
it "creates a successful mesaage post" do
#post = Post.create(message: "Message")
#post.should be_an_instance_of Post
end
end
end
I am getting failures on both. Please take a look on the code and help me figure out.
I suspect you are not logged in since you are using Devise?
Maybe you need to include the devise testhelpers:
describe PostsController do
include Devise::TestHelpers
before(:each) do
#user = User.create(...)
sign_in #user
end
#assertions go here
end
As Tigraine states, it appears as though you probably are not logged in (with Devise) when the tests get executed. However, showing the failures would help in narrowing down the problem further.
On top of that, the second test isn't really an integration test and I would probably prefer something like the following to test the same condition. There are two types of test you could do:
# inside 'describe "#create"'
let(:valid_params) { {'post' => {'title' => 'Test Post'} }
it 'creates a new Post' do
expect {
post :create, valid_params
}.to change(Post, :count).by(1)
end
# and / or
it 'assigns a new Post' do
post :create, valid_params
assigns(:post).should be_a(Post)
assigns(:post).should be_persisted
end
Don't forget to add this line into your spec_helper.rb
require "devise/test_helpers"
include Devise::TestHelpers
Nevertheless, here is link for Devise wiki - How to test Controllers where you can find more info about this approach. I recommend writing the before method without (:each), what I remember it sometimes causes problems.
before do
#user = FactoryGirl.create(:user)
sign_in #user
end
Can always use:
puts response.inspect
To see how your response looks like.

Resources