Very basic controller spec failing because something with my strong parameters set up is wonky?
class OrdersController
def create
#order = Order.new(order_params)
if #order.valid?
...
end
private
def order_params
params.require(:order).and_permit(:email)
end
end
Test code:
describe OrdersController, "Create action", type: :controller do
it "should call valid? method" do
Order.any_instance.should_receive(:valid?)
post :create, order: {email: "test#example.com"}
end
end
Outcome:
Failure/Error: post :create, order: {email: "test#example.com"}
NoMethodError:
undefined method `and_permit' for {"email"=>"test#example.com"}:ActionController::Parameters
the comment on the original post from jvnill hit the nail on the head, it is permit not and_permit
params.require(:order).permit(:email)
Related
I have two problems when I try to test the update action with RSpec, here is the controller file:
#volunteers_controller.rb
module Api
module V1
class VolunteersController < ApplicationController
before_action :find_volunteer, only: %i[show update destroy]
def update
#volunteer.update!(volunteer_params)
head :no_content
end
private
def find_volunteer
#volunteer = Volunteer.find_by!(id: params[:id])
end
def volunteer_params
params.require(:volunteer).permit(:image_url, :name, :job_desc)
end
end
end
end
Here is the test file:
require 'rails_helper'
RSpec.describe Api::V1::VolunteersController, type: :request do
...
describe '#update' do
let(:volunteer) { Volunteer.create!( :image_url=>"first.jpg", :name=>"test1", :job_desc=>"description") }
let(:params){
{:volunteer => {
"image_url"=>"new.jpg",
"name"=>"test1",
"job_desc"=>"description"
}
}
}
it 'updates a certain volunteer' do
patch :patch, :params => params #failed, bad URL
expect(volunteer.image_url).to eq("new.jpg") #failed, still return 'first.jpg'
end
it 'returns a no_content header' do
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
expect(response).to have_http_status "204"
end
end
end
private
def json_parse(string)
if string.class==String
json = JSON.parse(string)
end
json
end
So my questions are:
when try to write the URL like this: patch :patch, :params => params, I got the following error:
Api::V1::VolunteersController#update updates a certain volunteer
Failure/Error: patch :patch, :params => params
URI::InvalidURIError:
bad URI(is not URI?): "http://www.example.com:80patch"
How can I change the URL to: "http://localhost:3000/api/v1/volunteers/#{volunteer.id}"?
I manually test the update action, putting a binding.pry in the update action, it does update volunteer subject, however, when it goes back to the test, it shows that it doesn't not get updated, why is that?
Thank you!!
The first problem is really your update method itself and its complete lack of error handling and meaningful feedback to the client. update! will raise ActiveRecord::RecordInvalid if the input is invalid - which is not rescued at all in your controller. And exceptions should no be used for normal code flow - invalid input is not really an exceptional event.
Instead you should rewrite your controller so that it checks if the update is performed and returns the appropriate response:
def update
if #volunteer.update(volunteer_params)
head :no_content
else
head :unprocessable_entity
end
end
As for the spec itself you're mixing up controller specs and request specs. While they look somewhat similar the key difference is that a request spec sends actual HTTP requests your rails server while a controller spec stubs the actual request and passes it to an instance of the controller under test.
In a controller spec you could write:
patch :update, params: { ... }
Because its actually calling the update method on an instance of the controller. But of course:
patch :patch, :params => params #failed, bad URL
Will not work in request spec since its not a valid URL and request specs send actual HTTP requests. Note that you should pass relative URLs and not absolute URLs as the test server may run on a different port then the dev server
# Bad
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
# Good
patch "/api/v1/volunteers/#{volunteer.id}", params: params
ActiveRecord models are not "live reloading" - the representation in memory will not automatically be updated when the values in the database are updated. You need to manaully reload the record for that to happen:
it 'updates a certain volunteer' do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
volunteer.reload
expect(volunteer.image_url).to eq("new.jpg")
end
Altogether your spec should actually look something like:
# Describe the endpoint - not the controller implmentation
RSpec.describe "V1 Volunteers API", type: :request do
describe 'PATCH /api/v1/volunteers/:id' do
# use do ... end if the expression does not fit on one line
let(:volunteer) do
# enough with the hashrockets already!
Volunteer.create!(
image_url: "first.jpg",
name: "test1",
job_desc: "description"
)
end
context "with invalid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
name: ""
}
}
end
it "returns unproccessable entity" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :unproccessable_entity
end
it "does not update the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to_not change(volunteer, :name).to("")
end
end
context "with valid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
image_url: "new.jpg",
name: "test1",
job_desc: "description"
}
}
end
it "returns no content" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :no_content
end
it "updates the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to change(volunteer, :image_url)
.to("new.jpg")
end
end
end
end
Using Rails 5.1.4, Ruby 2.4.1, rspec
Scenario:
In article destroy allow only user current_ma_user with role "a,m"
Then:
Check if current_ma_user.role = "a,m"
or current_ma_user own article (#article.user)
So I create current_ma_user as hash as well as user.
Then call role to check what is user[role ]
Problems:
How to add new method to hash.
How to pass that hash.method from rspec controller_spec to controller.
Failures:
1) ArticlesController DELETE #destroy destroys the requested article
Failure/Error: delete :destroy, params: {id: article.to_param}, session: valid_session, :current_ma_user.role => "a,m"
NoMethodError:
undefined method `role' for :current_ma_user:Symbol
# ./spec/controllers/articles_controller_spec.rb:172:in `block (4 levels) in <top (required)>'
# ./spec/controllers/articles_controller_spec.rb:171:in `block (3 levels) in <top (required)>'
This is the gist
articles_controller_spec.rb:
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
class Hash #patch to temp pass problem 1
def role
"a,m" #Hard Code, need to call user["role"] need code
end
end
user = {}
user["uid"] = "admin"
user["provider"] = "Facebook"
user["email"] = "1.0#kul.asia"
user["role"] = "a,m"
current_ma_user = user
describe "DELETE #destroy" do
it "destroys the requested article" do
article = Article.create! valid_attributes
expect {
delete :destroy, params: {id: article.to_param}, session: valid_session
}.to change(Article, :count).by(-1)
end
it "redirects to the articles list" do
article = Article.create! valid_attributes
delete :destroy, params: {id: article.to_param}, session: valid_session
expect(response).to redirect_to(articles_url)
end
end
end
Controller:
class ArticlesController < ApplicationController before_action :load_article, only: [:show, :destroy]
def destroy
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
#if current_ma_user == #article.user
#article.destroy
end
redirect_to :action=>'index' end
private
def load_article
#article = Article.find(params[:id]) end
end
Updated with line number:
Updated debug to show value of current_ma_user in .spec and controller
This is where your error is coming from (in your controller):
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
Suggested Solutions
Where is current_ma_user defined in the controller? (if it’s not assigned, then it needs to be assigned before you call the role method on the current_ma_user variable.
Try that and see how it goes.
Do something like this:
current_ma_user = User.find( params[:user_id])
Now you seem to want to pass something into the params hash. Remember to white list whatever you decide to pass into params. Whether it is user id or roles id etc, or a roles string.
When writing your tests, pass in the approrpiate values to the params hash. If you are passing in a user_id in your test, then you will have to make sure that a user is created in the test.
delete :destroy, {:id => article.id.to_s, :user_id => #current_ma_user.id }, session: valid_session
also perhaps in your spec file, in your test, put the current_ma_user in a before filter and make it an instance variable so it will be accessible to all your tests:
before(:each) do
#current_ma_user = user.create( <--- create the user with the
appropriate attributes here --->)
end
Warning: Untested
I just typed it into the stack overflow editor.
Hi I am implementing a method to delete a user account in my web application. My controller:
class UsersController < ApplicationController
before_filter :set_current_user
def user_params
params.require(:user).permit(:user_id, :first_name, :last_name, :email, :password, :password_confirmation)
end
def delete_account
#user = User.find_by_id(params[:id])
if #user.present?
#user.destroy
flash[:notice] = "User Account Deleted."
end
redirect_to root_path
end
def destroy
User.delete(:user_id)
redirect_to root_path
end
end
My rspec:
require 'spec_helper'
require 'rails_helper'
require'factory_girl'
describe UsersController do
describe "delete account" do
before :each do
#fake_results = FactoryGirl.create(:user)
end
it "should call the model method that find the user" do
expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
end
it "should destroy the user account from the database" do
expect{delete :destroy, id: #fake_results}.to change(User, :count).by(-1)
end
it "should redirect_to the home page" do
expect(response).to render_template(:home)
end
end
end
The first error is
Failure/Error: expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
NameError:undefined local variable or method `params' for #<RSpec::ExampleGroups::UsersController::DeleteAccount:0x00000007032e18>
I know what this error means but I don't know how to correct it. How can I pass the user id from the controller to rspec?
The second error is:
Failure/Error: expect(response).to render_template(:home)
expecting <"home"> but rendering with <[]>
I think there is something wrong with my controller method. It should redirect to the home page but it doesn't.
params is not available in your tests, it's available in your controller.
Looks like you create a test user in your test:
#fake_results = FactoryGirl.create(:user)
Then, you can use the id of this test user (#fake_results.id) instead of trying to use params[:id]:
expect(User).to receive(:find).with(#fake_results.id).and_return (#fake_results)
Although, you may want to change the name from #fake_results to something more meaningful e.g. test_user or so.
However, this should fix both of your problems as your second problem is there because of the first problem. As it's failing to delete the user in the first place, it's not being redirected to the root path and hence the home template is not rendering.
I Added customer validation in my controller like
def customer_create
if params[:api_key].present?
## Create Customer
else
## Render Error Message in Json format
end
end
How can I write Rspec Testing for above method?
Thanks In Advance
presumably customer_create is called from your controller's create action?
So it might be sufficient to simply test that action.
describe CustomersController do
it "creates customer if :api_key is present` do
post :create, api_key: "present key", customer_attributes
expect(Customer.count).to eq 1
end
it "does not create customer if :api_key is absent` do
json_error = {
key1: 'value1',
key2: 'value2'
}.to_json
post :create, customer_attributes
expect(response.body).to eq json_error
end
end
You can test the method directly, if you set up the params.
describe CustomersController do
it "creates customer if :api_key is present' do
controller.params[:api_key] = 'present key'
controller.params.merge!(customer_attributes)
controller.customer_create
expect(Customer.count).to eq 1
end
end
Both examples assume customer attributes hash is stored in a variable customer_attributes
I'm using rspec and Factory Girl for testing. When testing the POST #create section of my posts_controller I'm getting the error in the title.
Failures:
1) PostsController POST #create with valid attributes redirects to the post
Failure/Error: response.should redirect_to Post.last
Expected response to be a <redirect>, but was <200>
# ./spec/controllers/posts_controller_spec.rb:59:in `block (4 levels) in <top (required)>'
This is the code from the spec being tested. I'm sure it's not the most efficient way of doing this, but it does work.
def create
#post = Post.new(
:text => post_params[:text],
:embed => post_params[:embed],
:user => current_user,
:title => post_params[:title],
:tag_list => post_params[:tag_list],
:cagegory_ids => post_params[:category_ids]
)
if #post.save
redirect_to #post
else
render 'new'
end
end
...
private
def post_params
params.require(:post).permit(:title, :text, :embed, :user_id, :tag_list,
:category_ids => [])
end
Here's the factory.
FactoryGirl.define do
factory :post do
title { Faker::Lorem.characters(char_count = 20) }
text { Faker::Lorem.characters(char_count = 150) }
user
categories {
Array(5..10).sample.times.map do
FactoryGirl.create(:category)
end
}
end
end
And the relevant part of the spec
describe "POST #create" do
context "with valid attributes" do
it "saves the new post" do
expect{
post :create, post: FactoryGirl.create(:post).attributes
}.to change(Post,:count).by(1)
end
it "redirects to the post" do
post :create, post: FactoryGirl.create(:post).attributes
response.should redirect_to Post.last
end
end
end
The other test, "saves the new post," works fine. I've tried other variations of the redirect_to line, such as "redirect_to(posts_path(assigns[:post])) and it throws the same error.
Any ideas?
OK, I fixed my problem. It isn't pretty but it works.
The issue is with Factory Girl and associations, and I'm definitely not the first to have that problem, but none of the other solutions worked.
I ended up adding this before :each at the top of the POST #create section...
describe "POST #create" do
before :each do
Post.destroy_all
#cat = FactoryGirl.create(:category)
#newpost = FactoryGirl.build(:post)
#post_params = {category_ids: [#cat.id]}.merge(#newpost.attributes)
end
...
...to put the post params into a new hash that I could call later on in the code like this...
it "saves the new post" do
expect{
post :create, post: #post_params
}.to change(Post,:count).by(1)
end
it "redirects to the post" do
post :create, post: #post_params
response.should redirect_to Post.last
end
So this is solved. It adds a bit of overhead to the test but it works. I won't mark this as THE solution for a couple of days in case someone else comes around with some better, simpler code. I definitely welcome any more ideas.
I assume the problem is in factory. Most likely post instance didn't pass validations and controller renders new view, which is not redirect, but success 200. Add logger in the controller and take a look if record actually saves. You can also read through test log tail -f log/test.log.