I cannot seem to get the following worked out.
Spec: (spec/api/power_ups_spec.rb)
describe Api::PowerUpsController, :type => :controller do
describe "GET power_ups" do
it "returns all power-ups" do
FactoryGirl.create :power_up, name: "Increase rate of take", description: "You gain points more quickly"
FactoryGirl.create :power_up, name: "Decrease rate of give", description: "You lose points more slowly"
get api_power_ups_path, {}, { "Accept" => "application/json" }
expect(response.status).to eq 200
body = JSON.parse(response.body)
power_up_names = body.map { |m| m["title"] }
expect(power_up_names).to match_array(["Increase rate of take",
"Decrease rate of give"])
end
end
end
Routes:
Rails.application.routes.draw do
namespace :api do
resources :power_ups, only: [:index]
end
end
Controller (app/controllers/api/power_up_controller.rb):
module Api
class PowerUpsController < ApplicationController
include ActionController::MimeResponds
respond_to :json
def index
respond_with PowerUp.all
end
end
end
Rake Routes:
Prefix Verb URI Pattern Controller#Action
api_power_ups GET /api/power_ups(.:format) api/power_ups#index
Error message on running spec:
Failure/Error: get api_power_ups_path, {}, { "Accept" => "application/json" }
ActionController::UrlGenerationError:
No route matches {:action=>"/api/power_ups", :controller=>"api/power_ups"}
get api_power_ups_path
this isn't how to use get in a controller spec.
In a controller-spec, you assume that the class-under-test in the controller... so you use get to call the actual method on the controller.
In this case the method is called index (ie, you have def index), so to activate the test you just call:
get :index
you use the path-helpers only when you are referring to other paths - eg where you get redirected to etc.
Related
I've written a test to test the 404 page
pages_controller_spec.rb
RSpec.describe PagesController, type: :controller do
before :all do
Rails.application.config.action_dispatch.show_exceptions = true
Rails.application.config.consider_all_request_local = false
end
describe "status 404" do
it "respond with 404 if page is not found" do
get :help, params: { id: "foobar" }
expect(response.status).to eq(404)
end
end
end
The pages controller is simple and functions to render the static pages "/help" and "/about"
class PagesController < ApplicationController
def help
end
def about
end
end
The error handling is set up as follows
application_controller.rb
def not_found
raise ActionController::RoutingError.new("Not Found")
rescue
render_404
end
def render_404
render file: "#{Rails.root}/public/404", status: :not_found
end
The test result is
expected: 404
got: 200
Which I do not understand since "/help/foobar" does render the 404 page when I try it myself in the browser. I guess the problem could be that my "get" action in the test is formatted wrong but I'm not sure.
EDIT
config/routes.rb as requested
get "/about", to: "pages#about"
get "/help", to: "pages#help"
EDIT 2
Updated the test with the syntax from https://relishapp.com/rspec/rspec-rails/v/3-4/docs/routing-specs/route-to-matcher
The test now looks like this
RSpec.describe PagesController, type: :controller do
before :all do
Rails.application.config.action_dispatch.show_exceptions = true
Rails.application.config.consider_all_request_local = false
end
describe "status 404" do
it "respond with 404 if page is not found" do
expect(get("/foobar")).to route_to("application#not_found")
end
end
end
Unfortuenly this raises another error
ActionController::UrlGenerationError:
No route matches {:action=>"/foobar", :controller=>"pages"}
No route is matching which is kind of the point but the "not_found" method is being used for some reason
Change your routes to make your call work from the browser:
get "/help/:id", to: "pages#help"
If the test returns a 200, it's because it calls directly the help method from your controller without using the config/routes.rb file.
EDIT
Here is how you test your routing: https://relishapp.com/rspec/rspec-rails/v/3-4/docs/routing-specs/route-to-matcher
I have the following situation:
EDITED
In my routes.rb
namespace :api, defaults: { format: :json } do
namespace :v1 do
# the definitions of other routes of my api
# ...
match '*path', to: 'unmatch_route#not_found', via: :all
end
end
EDITED
My controller:
class Api::V1::UnmatchRouteController < Api::V1::ApiController
def not_found
respond_to do |format|
format.json { render json: { error: 'not_found' }, status: 404 }
end
end
end
My test is as shown:
require 'rails_helper'
RSpec.describe Api::V1::UnmatchRouteController, type: :controller do
describe 'get response from unmatched route' do
before do
get :not_found, format: :json
end
it 'responds with 404 status' do
expect(response.status).to eq(404)
end
it 'check the json response' do
expect(response.body).to eq('{"error": "not_found"}')
end
end
end
It seems right to me, however I got the same error for both it statments:
1) Api::V1::UnmatchRouteController get response from unmatched route responds with 404 status
Failure/Error: get :not_found, format: :json
ActionController::UrlGenerationError:
No route matches {:action=>"not_found", :controller=>"api/v1/unmatch_route", :format=>:json}
# /home/hohenheim/.rvm/gems/ruby-2.3.1#dpms-kaefer/gems/gon-6.1.0/lib/gon/spec_helpers.rb:15:in `process'
# ./spec/controllers/api/v1/unmatch_route_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
EDITED
The purpose with this route is be trigged when there's no other route possible in my api, with a custom json 404 response. This route and controller is working as expected right now, when we access routes like: /api/v1/foo or /api/v1/bar
How can I write the tests properly?
Additional info: Rails 4.2.6, Rspec 3.5.4
If you try to write routes spec, it won't work too and it will return something strange.
Failure/Error:
expect(get("/unmatch")).
to route_to("unmatch_route#not_found")
The recognized options <{"controller"=>"unmatch_route", "action"=>"not_found", "path"=>"unmatch"}> did not match <{"controller"=>"unmatch_route", "action"=>"not_found"}>, difference:.
--- expected
+++ actual
## -1 +1 ##
-{"controller"=>"unmatch_route", "action"=>"not_found"}
+{"controller"=>"unmatch_route", "action"=>"not_found", "path"=>"unmatch"}
Beside action not_found, it returned path => unmatch that maybe why controller spec didn't work as expected. Thus instead of controller test you can use request test as below.
require 'rails_helper'
RSpec.describe "get response from unmatched route", :type => :request do
before do
get '/not_found', format: :json
end
it 'responds with 404 status' do
expect(response.status).to eq(404)
end
it 'check the json response' do
expect(response.body).to eq('{"error": "not_found"}')
end
end
Take a look at this link:
https://apidock.com/rails/ActionDispatch/Routing/Mapper/Base/match
It says:
Note that :controller, :action and :id are interpreted as url query parameters and thus available through params in an action.
match ":controller/:action/:id"
Your route is:
match '*path', to: 'unmatch_route#not_found', via: :all
So your test is trying to find a route with :action=>"not_found" inside :controller=>"api/v1/unmatch_route". But your routes.rb does not have this route.
try something like this:
match 'unmatch_route/not_found', to: 'unmatch_route#not_found', via: :all
If you really need to use *path try this:
match '/:path/', :to => 'unmatch_route#not_found', :path=> /.*/, :as =>'not_found'
I also found myself wanting to test the response for API errors was rendering JSON, rather than writing a spec which simply rescued ActionController::RoutingError.
The following request spec worked for me, using Rails 6.0 & RSpec 3.9:
require 'rails_helper'
RSpec.describe '404 response for API endpoints' do
it 'renders an error in JSON' do
render_exceptions do
get '/api/v1/fictional-endpoint', headers: { 'Accept' => 'application/json' }
end
expect(response).to have_http_status(:not_found)
expect(response['Content-Type']).to include('application/json')
expect(json_response.fetch(:errors)).to include('Not found')
end
private
def json_response
JSON.parse(response.body, symbolize_names: true)
end
def render_exceptions
env_config = Rails.application.env_config
original_show_exceptions = env_config['action_dispatch.show_exceptions']
original_show_detailed_exceptions = env_config['action_dispatch.show_detailed_exceptions']
env_config['action_dispatch.show_exceptions'] = true
env_config['action_dispatch.show_detailed_exceptions'] = false
yield
ensure
env_config['action_dispatch.show_exceptions'] = original_show_exceptions
env_config['action_dispatch.show_detailed_exceptions'] = original_show_detailed_exceptions
end
end
References:
How to have Rails request specs handling errors like production
Comment regarding Rails.application.env_config caching
I'm trying to test the ArticlesController in my Rails applications. All of the routes that do not accept params are passing. But all of the routes that expect an id param are failing.
Failures:
1) ArticlesController should find article by id
Failure/Error: get :info, id: #article[:id]
ActionController::UrlGenerationError:
No route matches {:action=>"info", :controller=>"articles", :id=>"60"}
# ./spec/controllers/articles_controller_spec.rb:26:in `block (2 levels) in <top (required)>'
2) ArticlesController should export folder
Failure/Error: get :export_folder, id: #article[:id]
ActionController::UrlGenerationError:
No route matches {:action=>"export_folder", :controller=>"articles", :id=>"60"}
# ./spec/controllers/articles_controller_spec.rb:56:in `block (2 levels) in <top (required)>'
3) ArticlesController should export an article by id
Failure/Error: get :export, id: #article[:id]
ActionController::UrlGenerationError:
No route matches {:action=>"export", :controller=>"articles", :id=>"60"}
# ./spec/controllers/articles_controller_spec.rb:50:in `block (2 levels) in <top (required)>'
config/routes.rb
get '/articles/list' => 'articles#list', defaults: { format: :html }
get '/articles/trendlist' => 'articles#trendlist', defaults: { format: :html }
get '/articles/show/:id' => 'articles#show', defaults: { format: :html }, as: :show_article
get '/articles/index'
get '/articles/info/:id' => 'articles#info', as: :article_info
get '/articles/export/:id' => 'articles#export', as: :export_article
get '/articles/view/:id' => 'articles#view'
get '/articles/favorite/:id' => 'articles#favorite'
get '/articles/trending' => 'articles#trending', defaults: { format: :json }
get '/articles/deleted' => 'articles#deleted', defaults: { format: :json }
get '/articles/csv/:id' => 'articles#csv'
get '/articles/export_folder/:id' => 'articles#export_folder', as: :export_folder
spec/controllers/articles_controller.rb
require 'spec_helper'
describe ArticlesController do
before(:all) do
Article.destroy_all
Comfy::Cms::Layout.destroy_all
Comfy::Cms::Site.destroy_all
site = FactoryGirl.create(:cms_site)
layout = FactoryGirl.create(:cms_layout, site_id: site[:id])
#article = FactoryGirl.create(:cms_page, layout_id: layout[:id], site_id: site[:id])
end
it 'should index articles' do
get :index
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'application/json; charset=utf-8')
end
its 'should list articles' do
get :list
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'text/html; charset=utf-8')
end
it 'should find article by id' do
get :info, id: #article[:id]
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'application/json; charset=utf-8')
end
it 'should list deleted articles' do
get :deleted
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'application/json; charset=utf-8')
end
it 'should list trending articles' do
get :trending
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'application/json; charset=utf-8')
end
it 'should update trending articles' do
get :trendlist
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'text/html; charset=utf-8')
end
it 'should export an article by id' do
get :export, id: #article[:id]
expect(response.response_code).to eq(200)
expect(response.headers).to include( 'Content-Type' => 'text/csv; charset=utf-8')
end
it 'should export folder' do
get :export_folder, id: #article[:id]
response.response_code.should eq(200)
expect(response.headers).to include( 'Content-Type' => 'text/html; charset=utf-8')
end
end
rake routes
Prefix Verb URI Pattern Controller#Action
tags GET /tags(.:format) tags#index
articles_list GET /articles/list(.:format) articles#list
articles_trendlist GET /articles/trendlist(.:format) articles#trendlist
articles GET /articles/show/:id(.:format) articles/articles#show
articles_index GET /articles/index(.:format) articles#index
GET /articles/info/:id(.:format) articles/articles#info
GET /articles/export/:id(.:format) articles/articles#export
GET /articles/view/:id(.:format) articles/articles#view
GET /articles/favorite/:id(.:format) articles/articles#favorite
articles_trending GET /articles/trending(.:format) articles#trending
articles_deleted GET /articles/deleted(.:format) articles#deleted
GET /articles/csv/:id(.:format) articles/articles#csv
GET /articles/export_folder/:id(.:format) articles/articles#export_folder
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
include ArticlesHelper
before_action :set_default_response_format, except: [:pdf, :show, :list, :trendlist, :export_folder]
def index
#articles = SearchArticlesCommand.new(params).execute
end
def deleted
#dlist = Article.deleted.map(&:article_id)
render :ids, status: :ok
end
def info
id = params[:id].to_i
#article = Article.published.find_by(id: id)
end
def list
#articles = Article.folder
render 'articles/list'
end
def favorite
...
render json: { result: true, is_liked: "#{is_liked}" }
end
def view
...
render json: { result: true }
end
def trending
load_trending_articles
end
def trendlist
load_trending_articles
render 'articles/trendlist'
end
def export
id = params[:id].to_i
#article = Article.published.find_by(id: id)
render pdf: #article.label.gsub(/\s/, '_'),
template: 'articles/export.pdf.erb',
dispostion: 'attachment',
locals: { paragraphs: #article.paragraphs, images: #article.images }
That is not really what namespace is used for. You can read up more on it here. Use resources instead and specify member for the one with id:
resources :articles, only: [] do
collection do
get :list
get :trendlist
get :trending
get :deleted
end
member do
get :info
get :export
get :view
get :favorite
get :csv
get :export_folder
end
end
get 'articles/index', to: 'articles#index'
get 'articles/show/:id', to: 'articles#show'
If you look at the output of rake routes you can see that Rails is looking for articles/articles#show etc. namespace is for creating routes which live in a namespace (duh) such as /admin/tools which would root to Admin::ToolsController.
You can instead use scope which adds a url prefix but not the namespace or resources:
resources :articles, only: [:show, :index] do
member do
get 'info'
get 'export' # Use /articles/1.format instead.
get 'view' # Do you need this? Code smell!
get 'favorite' # should be post - GET should never create or alter a resource.
get 'csv' # remove - use /articles/1.csv instead
get 'show' # /articles/show/3
end
collection do
get 'trending'
get 'deleted'
get 'trendlist'
get 'list' # Do you need this? Code smell!
get 'index' # /articles/index
end
end
I would also question why you actually need so many routes beyond the standard CRUD set.
Especially routes which are extremely in semantics like view and show and list and index.
I would use query parameters around a smaller set of routes as it reduces the amount of duplication on all levels.
/articles?filter=deleted => index
/articles?filter=trending
Rails also has a built in CSV mime type so you can do:
/articles/5.csv
class ProductsController < ApplicationController
def show
#article = Article.order(:name)
respond_to do |format|
format.html
format.csv { render text: #article.to_csv }
end
end
end
Using #article[:id] vs #article.id does work but its unidiomatic and very slightly slower since rails has to go through the [] method just to find the getter method. Its not a huge deal in this case but not great when you are dealing with a large number of objects.
I'm creating a simple API with Rails and my test for creating a Goal resource is failing due to a RoutingError -- but I'm not sure why.
I can see when I do rake routes that the POST /goals route exists.
In my my routes.rb, I've set up namespace :api to be path: '/', so I think my post request should be working.
Can anyone help me point out what I've done wrong?
Here's my routes.rb
Rails.application.routes.draw do
with_options except: [:new, :edit] do |list_only|
namespace :api, path: '/', constraints: { subdomain: 'api' } do
list_only.resources :goal
end
end
end
Here are my routes:
$ rake routes
Prefix Verb URI Pattern Controller#Action
api_goals GET /goals(.:format) api/goals#index {:subdomain=>"api"}
POST /goals(.:format) api/goals#create {:subdomain=>"api"}
api_goal GET /goals/:id(.:format) api/goals#show {:subdomain=>"api"}
PATCH /goals/:id(.:format) api/goals#update {:subdomain=>"api"}
PUT /goals/:id(.:format) api/goals#update {:subdomain=>"api"}
DELETE /goals/:id(.:format) api/goals#destroy {:subdomain=>"api"}
My goals_controller.rb
class API::GoalsController < ApplicationController
before_action :set_goal, only: [:show, :edit, :update, :destroy]
def index
goals = Goal.all
if is_complete = params[:is_complete]
goals = goals.where(is_complete: is_complete)
end
render json: goals, status: 200
end
def show
goal = Goal.find(params[:id])
render json: goal, status: 200
end
def create
goal = Goal.new(goal_params)
if goal.save
# render nothing: true, status: 204, location: goal
head 204, location: goal
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_goal
#goal = Goal.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def goal_params
params.require(:goal).permit(:description, :motivation, :completion_date, :is_complete)
end
end
Here's the test:
require 'test_helper'
class CreatingGoalsTest < ActionDispatch::IntegrationTest
test 'creates goals' do
post '/goals',
{
description: "string",
motivation: "another string",
completion_date: "01/01/2014",
is_complete: true
}.to_json,
{ 'Accept' => Mime::JSON, 'Content-Type' => Mime::JSON.to_s }
assert_equal 201, response.status
assert_equal Mime::JSON, response.content_type
goal = json(response.body)
assert_equal goal_url(goal[:id]), response.location
end
end
Here's the error:
$ rake test:integration
Run options: --seed 27226
# Running:
E....
Finished in 0.102621s, 48.7230 runs/s, 116.9351 assertions/s.
1) Error:
CreatingGoalsTest#test_creates_goals:
ActionController::RoutingError: No route matches [POST] "/goals"
test/integration/creating_goals_test.rb:5:in `block in <class:CreatingGoalsTest>'
Thank you!
This can happen when the subdomain is not set in the test as rails is expecting it, you can override the host that is provided by adding the following:
def setup
host! 'api.example.com'
end
to your test suite.
class CreatingGoalsTest < ActionDispatch::IntegrationTest
def setup
host! 'api.example.com'
end
test 'creates goals' do
post '/goals',
{
description: "string",
motivation: "another string",
completion_date: "01/01/2014",
is_complete: true
}.to_json,
{ 'Accept' => Mime::JSON, 'Content-Type' => Mime::JSON.to_s }
assert_equal 201, response.status
assert_equal Mime::JSON, response.content_type
goal = json(response.body)
assert_equal goal_url(goal[:id]), response.location
end
end
Further resources on subdomains and testing in rails: Testing in Rails with Subdomains
I'm creating an API in Rails and I use versionist to handle versions. I want to test API controllers, but I'm unable to create a valid request.
My controller:
class Api::V1::ItemsController < Api::V1::BaseController
def index
render json:'anything'
end
end
My spec:
describe Api::V1::ItemsController do
describe "#create" do
it "shows items" do
get :index, format: :json
end
end
end
routes.rb:
scope '/api' do
api_version(:module => "Api::V1", :path => {:value => "v1"}, :default => true) do
resources :items
end
end
The test doesn't check anything. Still, it raises an error:
Failure/Error: get :index, format: :json
ActionController::RoutingError:
No route matches {:format=>:json, :controller=>"api/v1/items", :action=>"index"}
I suppose that there is something wrong with the :controller key in the request, but I don't know how to fix it...
I was able to reproduce this locally. You need to move this to a request spec instead of a controller spec for this to work:
# spec/requests/api/v1/items_controller_spec.rb
describe Api::V1::ItemsController do
describe "#index" do
it "shows items" do
get '/api/v1/items.json'
# assert something
end
end
end
The versionist documentation says you need to do this when using the HTTP header or request parameter versioning strategies (https://github.com/bploetz/versionist#a-note-about-testing-when-using-the-http-header-or-request-parameter-strategies) but that's clearly not the case here. I'll file an issue to get this clarified in the documentation that you need to do this for all versioning strategies.