I'm following along with a tutorial on testing with Rspec. Getting a syntax error when trying to run the test. Here's my test code:
require 'rails_helper'
RSpec.describe CommentsController, type: :controller do
describe "comments#create action" do
it "should allow admins to create comments on posts" do
post = FactoryGirl.create(:post)
admin = FactoryGirl.create(:admin)
sign_in admin
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to root_path
expect(post.comments.length).to eq 1
expect(post.comments.first.message).to eq "awesome gram"
end
it "should require an admin to be logged in to comment on a post" do
post = FactoryGirl.create(:post)
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
it "should return http status code of not found if the post isn't found" do
admin = FactoryGirl.create(:admin)
sign_in admin
post :create, params: { post_id: 'SUNSHINE', comment: { message: 'awesome post'} }
expect(response).to have_http_status :not_found
end
end
end
Here's the controller:
class CommentsController < ApplicationController
before_action :authenticate_admin!, only: [:create]
def create
#post = Post.find_by_id(params[:post_id])
return render_not_found if #post.blank?
#post.comments.create(comment_params.merge(admin: current_admin))
redirect_to root_path
end
private
def render_not_found(status=:not_found)
render plain: "#{status.to_s.titleize} :(", status: status
end
def comment_params
params.require(:comment).permit(:message)
end
end
Here's the terminal output when running the test:
Terminal output
What's odd is that when I comment the lines that are producing the error, the tests run as intended with the last test passing. I've checked the test file along with similar posts that describe the same problem and it looks to me like the syntax is correct. New to Rails so pardon the rookie mistakes.
The problem is that you have a variable named post:
post = FactoryGirl.create(:post) # define `post` variable
post :create, params: ... # try to call `post` method
Therefore, on subsequent lines post will refer to the variable, not the post method.
Solution 1: Rename the variable
my_post = FactoryGirl.create(:post)
post :create, params: { post_id: my_post.id, ... }
Solution 2: Use self.post to access the method
post = FactoryGirl.create(:post)
self.post :create, params: { post_id: post.id, ... }
Replicating the issue in irb:
def foo
"bar"
end
foo = "wat"
#=> "wat"
foo :hello
# SyntaxError: (irb):63: syntax error, unexpected ':', expecting end-of-input
# foo :hello
# ^
I think it has to do with you defining a variable named post then trying to call the Rspec method post:
it "should require an admin to be logged in to comment on a post" do
post = FactoryGirl.create(:post)
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
Try re-naming it:
to call the Rspec method post:
it "should require an admin to be logged in to comment on a post" do
message = FactoryGirl.create(:post)
post :create, params: { post_id: message.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
Related
I've got a simple message app to learn RSpec where one user can create message to another user (only logged users can write messages). I didn't used devise or FactoryBot, this app is as simple as possible just for rspec learning.
I wanted to run these tests for sessions controller, but the second one (when user has invalid params) gives me an error Expected response to be a <3XX: redirect>, but was a <200: OK> and I don't understand why since hours.
RSpec.describe SessionsController, type: :controller do
let(:create_user) { #user = User.create(username: 'John', password: 'test123') }
describe 'POST #create' do
context 'when user is logged in' do
it 'loads correct user details and redirect to the root path' do
create_user
post :create, params: { session: { username: #user.username, password: #user.password } }
expect(response).to redirect_to(root_path)
end
end
context 'when user has invalid params' do
before do
create_user
post :create, params: { session: { username: #user.username, password: 'somepass' } }
end
it 'render new action' do
expect(assigns(:user)).not_to eq create_user
expect(response).to redirect_to(action: 'new')
end
end
end
end
Sessions Controller
class SessionsController < ApplicationController
before_action :logged_in_redirect, only: %i[new create]
def new; end
def create
user = User.find_by(username: params[:session][:username])
if user && user.authenticate(params[:session][:password])
session[:user_id] = user.id
flash[:success] = 'You have successfully logged in'
redirect_to root_path
else
flash.now[:error] = 'There was something wrong with your login'
render 'new'
end
end
end
I'm not quite sure if line expect(assigns(:user)).not_to eq create_user is in line with convention but it doesn't matter for result.
In your test you expect redirect response:
expect(response).to redirect_to(action: 'new')
And in the controller you just render new template:
render 'new'
I think it's a good approach to render new, you should change your spec to expect this.
expect(response).to render_template(:new)
Here is my create action for users:
def create
#user = User.new(user_params)
if #user.save
respond_to do |format|
format.html {
redirect_to edit_admin_user_path(#user)
flash[:success] = "Successfully created"
}
end
else
render :new
flash[:alert] = "Something went wrong"
end
end
My test is looking like this:
context "POST methods" do
describe "#create" do
it "renders the edit template" do
post :create, user: FactoryGirl.attributes_for(:user)
expect(response).to render_template(:edit)
end
end
end
However I'm getting this error:
Failures:
1) Admin::UsersController POST methods #create renders the edit template
Failure/Error: expect(response).to render_template(:edit)
expecting <"edit"> but was a redirect to <http://test.host/admin/users/80/edit>
I want to check if the edit.html.haml file is rendered after creating a user. What am I doing wrong?
Update #1
I do check for redirect in another test, this is my full test suite:
require 'rails_helper'
RSpec.describe Admin::UsersController, type: :controller do
render_views
context "POST methods" do
describe "#create" do
it "using valid params" do
expect{
post :create, user: { email: "something#hello.com", password: "long12345678" }
}.to change(User, :count).by(1)
# get user_path('1')
end
it "redirects to the edit page after saving" do
post :create, user: FactoryGirl.attributes_for(:user)
user = User.last
expect(response).to redirect_to(edit_admin_user_path(user.id))
end
it "renders the edit template" do
post :create, user: FactoryGirl.attributes_for(:user)
user = User.last
expect {
redirect_to(edit_admin_user_path(user.id))
}.to render_template(:edit)
end
context "it redirects to new" do
it "if user has no valid mail" do
post :create, user: { email: "something", password: "long12345678" }
expect(response).to render_template(:new)
end
it "if user has no valid password" do
post :create, user: { email: "something#mail.com", password: "short" }
expect(response).to render_template(:new)
end
end
end
end
end
What I want is to actually check if the edit template is rendered. Because with expect(response).to redirect_to(edit_admin_user_path(user)) it does not check the template. This test passes even if I have no edit.html.haml file at all.
When you're testing create action you should just check correctness of redirect. In this action you're not actually rendering edit template, but you're just making redirect to the edit path of created entity. So this is the thing you should check.
describe "#create" do
it "redirects to the edit path" do
post :create, user: FactoryGirl.attributes_for(:user)
expect(response).to redirect_to(edit_admin_user_path(User.last))
end
end
Then you should have another test for edit action, where you're checking template rendering. That will mean that after redirect in create action you also will see the proper template.
You are redirecting to edit_admin_user_path after successfully saving the User in your controller action. But, you're testing render in the test instead.
Update your test as below.
context "POST methods" do
describe "#create" do
before(:each) do
#user = FactoryGirl.attributes_for(:user)
end
it "renders the edit template" do
post :create, user: #user
expect(response).to redirect_to(edit_admin_user_path(#user))
end
end
end
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.
I have a problem with redirecting and testing it in rspec
I have a problem with test not passing when I use a get method, but are green for the same code base when I use a put method. I don't know how to work around this and need assistance in making tests pass.
I get a 200 HTTP status code, but I want to get confirmation on redirect, so that it could be tracked by rspec. What code basically needs to do is redirect logged in user who is not owner of a product to category_product_url(category, product) with a flash error: 'You are not allowed to edit this product.' when trying to edit product with get http method.
Used gems rspec-rails, devise and decent_exposure. Ruby 2.1.5 and Rails 4.1.8
Actual error message:
Failure/Error: expect(response).to redirect_to(category_product_url(category, product))
Expected response to be a <redirect>, but was <200>
Failure/Error: expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
expected: "You are not allowed to edit this product."
got: nil
(compared using ==)
My spec
context 'another user is singed in' do
let(:user) { create(:user) }
let(:user2) { build(:user) }
let(:product) { Product.create! valid_attributes }
before do
sign_in user2
controller.stub(:user_signed_in?).and_return(true)
controller.stub(:current_user).and_return(user2)
controller.stub(:authenticate_user!).and_return(user2)
product.user = user
end
describe 'GET edit' do
describe 'with valid params' do
it 'redirects to product page' do
get :edit, { id: product.to_param, category_id: category.to_param }
expect(response).to redirect_to(category_product_url(category, product))
end
it 'renders error message' do
get :edit, { id: product.to_param, category_id: category.to_param }
expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
end
end
end
My controller
before_action :authenticate_user!, only: [:new, :edit, :update, :destroy, :create]
expose(:category)
expose(:products)
expose(:product)
def edit
end
def update
if product.user == current_user
if self.product.update(product_params)
redirect_to category_product_url(category, product), notice: 'Product was successfully updated.'
else
render action: 'edit'
end
else
redirect_to category_product_url(category, product), flash: { error: 'You are not allowed to edit this product.' }
end
end
private
def product_params
params.require(:product).permit(:title, :description, :price, :category_id, :user_id)
end
What is strange, is that put method is working fine with the same update action. Following specs are passing
describe 'PUT update' do
describe 'with valid params' do
it 'redirects to product page' do
put :update, { id: product.to_param, product: { 'title' => 'MyString' }, category_id: category.to_param }
expect(response).to redirect_to(category_product_url(category, product))
end
it 'does not update product' do
put :update, { id: product.to_param, product: { 'title' => 'MyNewString' }, category_id: category.to_param }
expect(controller.product.title).to_not eq 'MyNewString'
end
it 'renders error message' do
put :update, { id: product.to_param, product: { 'title' => 'MyString' }, category_id: category.to_param }
expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
end
end
end
The way how decent_exposure and devise work, you need to first invoke a before_action method passing there a private method of your choice.
For this particular example your controller should contain this
before_action :author!, only: [:edit, :update]
and a private method filter
def author!
unless self.product.user == current_user
redirect_to category_product_url(category, product),
flash: { error: 'You are not allowed to edit this product.' }
end
end
This way you get your get http requests and put http requests pass the specs.
Try this
expect(:get => "/products/1/edit").to route_to(:controller => "controller_name", :action => "action_name")
Hope this will work.
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