Controller RSpec: test PATCH update using custom ID - ruby-on-rails

I’m having a challenge write a RSpec controller test for a PATCH update, because the routing and edit uses a secure edit_id that my model generates, instead of the standard 1,2,3,4,5 (sequenced id) that Rails auto-generates. Basically, I’m not sure how to get my tests to lookup the request to be edited using this edit_id.
My test currently:
describe "PATCH edit/update" do
before :each do
#testrequest = FactoryGirl.build(:request, name: "James Dong")
end
it "located the requested #testrequest" do
patch :update, id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request)
assigns(:request).should eq(#testrequest)
end
describe "using valid data" do
it "updates the request" do
patch :update, #testrequest.name = "Larry Johnson"
#testrequest.reload
#testrequest.name.should eq("Larry Johnson")
end
end
FactoryGirl helper (I've tried both explicitly adding edit_id and not [i.e., relying on the model to create the edit_id itself], neither makes a difference) code:
FactoryGirl.define do
factory :request do |f|
f.name { Faker::Name.name }
f.email { Faker::Internet.email }
f.item { "random item" }
f.detail { "random text" }
f.edit_id { "random" }
end
end
Controller:
def update
#request = Request.find_by_edit_id(params[:edit_id])
if #request.update_attributes(request_params)
flash[:success] = "Your request has been updated! We'll respond within one business day."
redirect_to edit_request_path(#request.edit_id)
else
render 'edit'
end
end
Routing:
get 'edit/:edit_id', to: 'requests#edit', as: 'edit_request'
patch 'requests/:edit_id', to: 'requests#update', as: 'request'

Ok someone helped me figure this out, and I feel very silly. The "id" that you pass to the patch method can be any id, so instead of trying to set id: edit_it, I should use edit_it in the first place. I.e., the code that works:
before :each do
#testrequest = FactoryGirl.build(:request, name: "James Dong")
end
it "located the requested #testrequest" do
patch :update, edit_id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request)
assigns(:request).should eq(#testrequest)
end
describe "using valid data" do
it "updates the request" do
patch :update, edit_id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request, name: "Larry Johnson")
#testrequest.reload
#testrequest.name.should eq("Larry Johnson")
end
end

Related

In RSpec, running a patch or put test will result in an ActionController::UrlGenerationError: No route matches error

What I want to solve
I want the Rspec patch or put test to succeed.
I also tested PostsContoroller before this, and I am puzzled because I did not get the same error when testing PostsContoroller.
Error
Failures:
1) Api::V1::PostItemsController update Update Content
Failure/Error: patch :update, params: { post: post_params }
ActionController::UrlGenerationError:
No route matches {:action=>"update", :controller=>"api/v1/post_items", :post=>{:id=>1, :content=>"Update-Content", :status=>false, :post_id=>1}}
# ./spec/controllers/post_items_spec.rb:11:in `block (3 levels) in <main>'
Finished in 0.35529 seconds (files took 5.58 seconds to load)
5 examples, 1 failure
Code
FactoryBot
book.rb
FactoryBot.define do
factory :book, class: Post do
sequence(:id) { |n| n}
sequence(:title) { |n| "title#{n}" }
sequence(:author) { |n| "author#{n}" }
sequence(:image) { |n| "image#{n}"}
end
end
content.rb
FactoryBot.define do
factory :content, class: PostItem do
sequence(:id) { |n| n }
sequence(:content) { |n| "list#{n}"}
sequence(:status) { false }
end
end
Spec
post_items_spec.rb
require 'rails_helper'
RSpec.describe Api::V1::PostItemsController, type: :controller do
describe 'update' do
it 'Update Content' do
book = create(:book)
content = create(:content, post_id: book.id)
post_params = { id: content.id, content: 'Update-Content', status: false, post_id: book.id }
patch :update, params: { post: post_params }
json = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(json['Update-Content']).to eq('Update-content')
end
end
end
Routes
**Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts
resources :post_items
end
end
end
The use of controller specs is discouraged by both the Rails and RSpec teams and has been for a very long time now. You should be writing a request spec instead which sends real HTTP requests.
RSpec.describe 'Api V1 Post items', type: :request do
let(:book) { create(:book) }
describe "PATCH /api/v1/books" do
context "with valid parameters" do
subject do
patch api_v1_post_item_path(book),
params: { content: 'Update-Content' }
end
it { should be_successful }
it "updates the content" do
# refresh the record from the db
expect { book.reload }.to change(book, :title).to('Update-Content')
end
it "includes the updated entity in the response body" do
expect(response.parsed_body['content']).to eq 'Update-Content'
end
end
# #todo write specs with invalid parameters
# #todo write specs for authentication and authorization
end
end
Another problem is that you're generating IDs in your factory. Do not do this ever. When you're actually persisting records the database will automatically assign ids. When you use build_stubbed FactoryBot will create a mock id. Using a sequence to generate IDs invites bad practices such as hardcoding ids into a spec and will only cause you headaches.
If you really want to salvage that controller spec the routing error is caused by the fact that you're missing an the ID parameter - since you're calling it as patch :update, params: { post: post_params } the id parameter is buried in params[:post][:id]. So you want patch :update, params: { id: post.id, post: post_params } I don't recommend this though - get with the program and write future proof tests instead that won't let all the bugs slip though.

test update request with RSpec failed

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

RSpec: How can I make one shared_example test that tests many different controller PATCH requests that all update different attributes?

I have various controllers and I have tests for each one that test their update action. The tests have all the exact same structure: it tests if it a patch request will update and change the object, or not. The only difference between these tests are which attribute the tests check to see for its change assertion. These attributes are unique to the controllers.
class CarsController
def update
# update attribute
...
end
end
and
class DogsController
def update
# update attribute
...
end
end
My tests (2 of many more):
CarsSpec
describe "PATCH" do
it "should update the car" do
expect do
patch :update, id: object.id, data: {make: "honda"}
end.to change {object.reload.make}
end
end
DogSpec
describe "PATCH" do
it "should update the dog" do
expect do
patch :update, id: object.id, data: {breed: "husky"}
end.to change {object.reload.breed}
end
end
As you can see, they're the exact same structure of tests so naturally, to be DRY I want to extract them into a shared_example to be DRY. The idea is to have many more of these controllers but only actual test and that these controllers just pass in which field to update. It would involve something like
shared_example "update" do
it "updates the object" do
expect do
patch :update, id.object.id, data: { customField: "new value" }
end.to change { object.reload.customField }
end
end
Where customField could be either breed or make in this case and the test would know to update those fields so it is generic and can apply to many of these controllers. How can I achieve that or something similar?
shared_example’s block accepts arguments:
shared_example "update" do |custom_field|
it "updates the object (field: #{custom_field})" do
expect do
patch :update, id.object.id, data: { custom_field => "new value" }
end.to change { object.reload.public_send(custom_field) }
end
end
and call it as
include_examples 'update', :make
include_examples 'update', :breed

Rspec prevent method from being called

I am testing a controller method for creating new orders (e-commerce-like app). If user is present in the system, he should be redirected to new_user_session_path, else to new_order_path. Simple as that.
This is my orders_controller.rb
def new
if !User.where(phone: params[:phone]).blank? && !user_signed_in?
redirect_to new_user_session_path()
flash[:info] = "Already present"
else
#order = Order.new
#menu = Menu.find(params[:menu_id])
#menu_price = #menu.calculate_price(#menu, params)
end
end
In my app, I need the calculate_price method to be called, because it calculates the overall price given the params. But in my test, I just want to ensure, that the redirect is correct.
Right now I'm getting errors like (they are sourced inside the Menu.rb file, since calculate_price is called) :
Front::OrdersController#new redirects user to new order page if user is not present in the system
Failure/Error: menu_price_change = menu_amount.split(",")[1].gsub(" ","").gsub("]",'')
NoMethodError:
undefined method `split' for nil:NilClass
This is my spec file:
require 'rails_helper'
describe Front::OrdersController, type: :controller do
describe '#new' do
# Set up dummy menu
let (:menu) { Menu.create() }
it "redirects user to sign up page if user is present in the system" do
user = User.create(name: "Bob", password: "bobspassword", phone: "+7 (903) 227-8874")
get :new, params: { phone: user.phone }
expect(response).to redirect_to(new_user_session_path(phone: user.phone))
end
it "redirects user to new order page if user is not present in the system" do
non_present_phone = "+7 (903) 227-8874"
get :new, params: { phone: non_present_phone, menu_id: menu.id}
expect(response).to redirect_to(new_order_path)
end
end
end
Of course I could provide all the params, but there is a pretty big amount of them and besides, I just want to test the correct redirect. As far as I know, mocks and subs are useful in this case, when you want to explicitly test the methods. But in my case, I want to - somehow - omit them. How can I ensure that behaviour?
So you want just to test redirects and the errors occured when calculate_price method executes bother you. Why don't you just stub that method? Your spec file might be like this:
require 'rails_helper'
describe Front::OrdersController, type: :controller do
describe '#new' do
# Set up dummy menu
let (:menu) { Menu.create() }
# Check this out
before do
allow_any_instance_of(Menu).to receive(:calculate_price)
# or if you need certain value
allow_any_instance_of(Menu).to receive(:calculate_price).and_return(your_value)
end
it "redirects user to sign up page if user is present in the system" do
user = User.create(name: "Bob", password: "bobspassword", phone: "+7 (903) 227-8874")
get :new, params: { phone: user.phone }
expect(response).to redirect_to(new_user_session_path(phone: user.phone))
end
it "redirects user to new order page if user is not present in the system" do
non_present_phone = "+7 (903) 227-8874"
get :new, params: { phone: non_present_phone, menu_id: menu.id}
expect(response).to redirect_to(new_order_path)
end
end
end

ActionController::UrlGenerationError, No route matches

I've read through every similar question I could find and still can't figure out my problem.
# routes.rb
Rails.application.routes.draw do
resources :lists, only: [:index, :show, :create, :update, :destroy] do
resources :items, except: [:new]
end
end
# items_controller.rb (excerpt)
class ItemsController < ApplicationController
...
def create
#list = List.find(params[:list_id])
...
end
...
end
# items_controller_spec.rb (excerpt)
RSpec.describe ItemsController, type: :controller do
...
let!(:list) { List.create(title: "New List title") }
let(:valid_item_attributes) {
{ title: "Some Item Title", complete: false, list_id: list.id }
}
let!(:item) { list.items.create(valid_item_attributes) }
describe "POST #create" do
context "with valid params" do
it "creates a new item" do
expect {
post :create, { item: valid_item_attributes, format: :json }
}.to change(Item, :count).by(1)
end
end
end
...
end
And the RSpec error:
1) ItemsController POST #create with valid params creates a new item
Failure/Error: post :create, { item: valid_item_attributes, format: :json }
ActionController::UrlGenerationError:
No route matches {:action=>"create", :controller=>"items", :format=>:json, :item=>{:title=>"Some Item Title", :complete=>false, :list_id=>1}}
The output from rake routes:
list_items GET /lists/:list_id/items(.:format) items#index
POST /lists/:list_id/items(.:format) items#create
edit_list_item GET /lists/:list_id/items/:id/edit(.:format) items#edit
list_item GET /lists/:list_id/items/:id(.:format) items#show
PATCH /lists/:list_id/items/:id(.:format) items#update
PUT /lists/:list_id/items/:id(.:format) items#update
DELETE /lists/:list_id/items/:id(.:format) items#destroy
I can successfully create a new item in an existing list via curl which tells me that the route is ok, I must be doing something wrong in my test.
curl -i -X POST -H "Content-Type:application/json" -H "X-User-Email:admin#example.com" -H "X-Auth-xxx" -d '{ "item": { "title": "new item", "complete": "false"} }' http://localhost:3000/lists/5/items
I am really confused. My routes are setup correctly. A ItemsController#create method definitely exists. The rest of the tests in items_controller_spec.rb pass without issue.
Am I missing something obvious?
Here are the fixes I had to make to my tests (items_controller_spec.rb). I was not passing the correct hash to post create:.
describe "POST #create" do
context "with valid params" do
it "creates a new item" do
expect {
post :create, { list_id: list.id, item: valid_item_attributes, format: :json }
}.to change(Item, :count).by(1)
end
it "assigns a newly created item as #item" do
post :create, { list_id: list.id, item: valid_item_attributes, format: :json }
expect(assigns(:item)).to be_a(Item)
expect(assigns(:item)).to be_persisted
end
end # "with valid params"
context "with invalid params" do
it "assigns a newly created but unsaved item as #item" do
post :create, { list_id: list.id, item: invalid_item_attributes, format: :json }
expect(assigns(:item)).to be_a_new(Item)
end
it "returns unprocessable_entity status" do
put :create, { list_id: list.id, item: invalid_item_attributes, format: :json }
expect(response.status).to eq(422)
end
end # "with invalid params"
end # "POST #create"
I've received the same error and fixed it in a different way. I'm using Rails ~> 5.0.7.
ROUTES
This is the output from running rake routes CONTROLLER=bills:
Prefix Verb URI Pattern Controller#Action
download_site_bill GET /sites/:site_id/bills/:id/download(.:format) bills#download
site_bills GET /sites/:site_id/bills(.:format) bills#index
POST /sites/:site_id/bills(.:format) bills#create
new_site_bill GET /sites/:site_id/bills/new(.:format) bills#new
edit_site_bill GET /sites/:site_id/bills/:id/edit(.:format) bills#edit
site_bill GET /sites/:site_id/bills/:id(.:format) bills#show
PATCH /sites/:site_id/bills/:id(.:format) bills#update
PUT /sites/:site_id/bills/:id(.:format) bills#update
DELETE /sites/:site_id/bills/:id(.:format) bills#destroy
bills GET /bills(/page/:page)(.:format) bills#index
download_bill GET /bills/:id/download(.:format) bills#download
GET /bills(.:format) bills#index
POST /bills(.:format) bills#create
new_bill GET /bills/new(.:format) bills#new
edit_bill GET /bills/:id/edit(.:format) bills#edit
bill GET /bills/:id(.:format) bills#show
PATCH /bills/:id(.:format) bills#update
PUT /bills/:id(.:format) bills#update
DELETE /bills/:id(.:format) bills#destroy
REFERENCE CODE
# controllers/admin/bills_controller.rb (excerpt)
module Admin
class BillsController < ApplicationController
...
def edit
authorize(#bill)
end
end
end
# spec/controllers/admin/bills_controller_spec.rb (excerpt)
require 'rails_helper'
RSpec.describe Admin::BillsController, type: :controller do
...
context 'as an AdminUser' do
login_admin_user
it 'loads the bill edit page' do
request.host = 'admin.example.com'
get :edit, { id: bill }
expect(response.status).to eq(200)
end
end
end
end
# Error message
2) Admin::BillsController GET bills/:id/edit as a User redirects to the home page
Failure/Error: get :edit
ActionController::UrlGenerationError:
No route matches {:action=>"edit", :controller=>"admin/bills"}
POTENTIAL SOLUTION
I tried variations of this solution. It may work for others, but I got this error:
Failure/Error: require 'admin/bills_controller'
LoadError:
cannot load such file -- admin/bills_controller
SOLUTION
I tried Case 2 in this github issues and it worked. The changes I made to my code were to add this params: { use_route: 'admins/bills/', id: bill.id }. Below is the context of this addition:
context 'as an AdminUser' do
login_admin_user
it 'loads the bill edit page' do
request.host = 'admin.example.com'
get :edit, params: { use_route: 'admins/bills/', id: bill.id }
expect(response.status).to eq(200)
end
end

Resources