Rails 4 - rendering JSON from a view - ruby-on-rails

I'm having the worst time rendering a .json.erb file from my controller while being able to test it with RSpec. I have api_docs/index.json.erb and the following controller:
class ApiDocsController < ApplicationController
respond_to :json
def index
render file: 'api_docs/index.json.erb', content_type: 'application/json'
end
end
The explicit render file line seems unnecessary, but if I don't do that or render template: 'api_docs/index.json.erb', then I get an error about "Missing template api_docs/index". Likewise if I do have to pass the file name, it sucks even more that I have to give the exact directory--Rails should know that my ApiDocsController templates live in the api_docs directory.
If I have render file or render template, then I can visit the page and get the JSON contents of my index.json.erb file, as expected. However, this RSpec test fails:
let(:get_index) { ->{ get :index } }
...
describe 'JSON response' do
subject {
get_index.call
JSON.parse(response.body)
}
it 'includes the API version' do
subject['apiVersion'].should_not be_nil
end
end
It fails on the JSON.parse(response.body) line and if I raise response.body, it's an empty string. If I do render json: {'apiVersion' => '1.0'}.to_json in the controller, then the test passes just fine.
So, how can I always render the JSON template when I go to /api_docs (without having to put .json at the end of the URL), and in a way that works both in the browser and in my RSpec test? And can I render the template without having to have some long render call in which I pass the full path of the view?

Actually since you're already using respond_to :json in your controller you can use just a render method to choose your template and, as you probably know, if the template have the same name of the controller method you should be able to suppress the whole render method.
If you just remove the render line, what's the result?

Part of my solution was based on this answer to another question: adding defaults: {format: :json} to my routes file lets me go to /api_docs and see the JSON when the action is just def index ; end with no render. The RSpec test still fails though. The full line from my routes file: resources :api_docs, only: [:index], defaults: {format: :json}.
Thanks to this guy with the same problem and his gist, I added render_views to my describe block and got my test to pass:
describe ApiDocsController do
render_views
...
let(:get_index) { ->{ get :index } }
describe 'JSON response' do
subject {
get_index.call
JSON.parse(response.body)
}
it 'includes the API version' do
subject['apiVersion'].should_not be_nil
end
end

Related

how to make a custom json rails routes and make the tests pass

Edit 2: OMG I AM SO STUPID. In my spec I have a let(:response) {MyModel.create()} so thats why its failing. Going to delete post
(edited for clarity)
In my routes file
root "search_email#index"
get "search_email/retrieve_last_user_survey" => "search_email#retrieve_last_user_survey"
Controller
class SearchEmailController < ApplicationController
def retrieve_last_user_survey
render :json => "")
end
end
Spec file
require "rails_helper"
RSpec.describe SearchEmailController, type: :controller do
describe 'GET #retrieve_last_user_survey' do
before do
get :retrieve_last_user_survey, :params => { :email => 'abc#abc.com'}
end
it "returns http success" do
expect(response).to have_http_status(:success)
end
end
end
When try to run my test, i get this
Failure/Error: expect(response).to have_http_status(:success)
expected a response object, but an instance of Relational::Response (custom model name) was received
I have no idea why I am not getting a response object, I know I am hitting the controller method cause I inserted puts and I can see it.
Also on a semi related note. If i create a button that hits this route. why does it redirect me to a show route. I thought it would just return some http request that i can see in the dev console. I know cause said I dont have a show route or a show template.
It's not meant to be facetious, but to get the test to pass, replace the render line in the controller with:
head :ok
Does the test pass? Probably. So now add some expectation on the content header, and then finally the content itself.
If you break it down into small pieces, you should find the problem. It's not obvious from what you've shared, we can't see into the controller method.

Rails testing render action in controller

I have a dynamically driven Rails application where views and determined by the path requested.
routes.rb:
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get ':tenant', to: 'tenants#home'
get '/:path', to: 'tenants#page', :constraints => { :path => /.*/ }
end
TenantsController:
class TenantsController < ApplicationController
def home
render :template => params[:tenant] + "/home"
end
def page
render :template => params[:path]
end
end
As you can see what simply happens is we get the path constraint from the endpoint and render a view template from it.
I would like to write a test that ensures rails requests a view template that matches the URL requested. (Basically test the page method within TenantsController).
Given that I do not want to tie my test into tenants that may be in the system, how can I write a test for this generically without the test knowing about some Tennant?
You can write tests for your controller using the assert_template method:
test "should render the correct template" do
['path1', 'path2'].each do |path|
get '/', params: { path: path }
assert_template "#{path}"
end
end
This guide provides more details on how to test your controllers.

Dynamic rendering with Rails 5 and Rspec

I am trying to test that my controller renders the correct status codes via request specs. This application uses a bit of meta-programming with dynamic class names to render views. How can I stub the render call below to return the correct status code desired for my spec?
Rspec Spec Snippet
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:template) { "providers/v1/#{provider_slug}/new" }
let(:layout) { "providers/v1/#{provider_slug}" }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(described_class).to receive(:render_new_form).and_return(true)
get route
end
it 'true' do
expect(response.status).to be(200)
end
end
Controller Snippet
class V1::ProvidersController < ApplicationViewController
before_action :init_provider, :init_form_types, :validate_provider
def new
#provider_form_model = provider_router.form_model.new
render_new_form
end
private
attr_reader :provider_slug, :provider_path, :provider_router, :provider_model
def render_new_form
render template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}"
end
Updated for Answer Below
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(controller).to receive(:provider_slug).and_return(provider_slug)
allow(controller).to receive(:render).and_call_original
allow(controller).to receive(:render).
with(template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}") do
controller.render plain: '200 [OK]'
end
get "/v1/providers/#{provider_slug}"
end
it 'true' do
expect(response.status).to be(200)
end
end
The formal answer would be that you shouldn't stub it as you would be stubbing behaviour of the object under test.
You should rather provide a provider_slug to be used for the test.
Technically, it would be possible to do this:
allow(controller) # controller is the instance of the ProvidersController used under the hood
.to_receive(:render_new_form)
.and_return("some bogus value")
But this would lead to rails trying to render the default template as no rendering has happened yet. It would thus be helpful to actually call the render method which can be achived by:
# we call the render method in our stub and thus have to be able to call the original
allow(controller)
.to receive(:render)
.and_call_original
allow(controller)
.to_receive(:render) # not render_new_form
.with(template: anything, layout: anything) do
controller.render plain: '200 [OK]'
end

rspec controller test for json api : ActionController::RoutingError

just a simple question for a render json call I'm trying to test. I'm still learning rspec, and have tried everything and can't seem to get this to work. I keep getting an ActionController::RoutingError, even though I defined the route and the call to the api itself works.
In my controller I have the method:
class PlacesController < ApplicationController
def objects
#objects = Place.find(params[:id]).objects.active
render json: #objects.map(&:api)
end
end
with the render json: #objects.map(&:api), I'm calling the api method in the Object model
class Object
def api
{ id: id,
something: something,
something_else: something_else,
etc: etc,
...
}
end
end
My routes file:
get "places/:id/objects" => "places#objects"
my rspec: spec/controllers/places_controller_spec.rb
describe "objects" do
it "GET properties" do
m = FactoryGirl.create :object_name, _id: "1", shape: "square"
get "/places/#{m._id}/objects", {}, { "Accept" => "application/json" }
expect(response.status).to eq 200
body = JSON.parse(response.body)
expect(body["shape"]).to eq "square"
end
end
I keep getting the error
Failure/Error: get "/places/1/objects", {}, { "Accept" => "application/json" }
ActionController::RoutingError:
No route matches {:controller=>"places", :action=>"/places/1/objects"}
Any help would be much appreciated, thanks.
Because you have the spec in the controllers folder RSpec is assuming it is a controller spec.
With controller specs you don't specify the whole path to the route but the actual controller method.
get "/places/#{m._id}/objects", {}
Should be
get :objects, id: m._id
If you don't want this behaviour you can disable it by setting the config infer_spec_type_from_file_location to false. Or you could override the spec type for this file by declaring the type on the describe
describe "objects", type: :request do - change :request to what you want this spec to be.
Although I recommend using the directory structure to dictate what types of specs you are running.

How to validate locals of render template in rspec

I wonder how to validate the locals passed to render template in controller
Controller:
def lelf_panel
# ...
if some_condition
locals_hash = some_very_long_hash_A
else
locals_hash = some_very_long_hash_B
end
render :partial => "left_panel", :layout => false, :locals => locals_hash
end
Current Spec:
it 'should render correct template for lelf_panel' do
# ...
get 'left_panel'
response.should render_template('system/_left_panel')
end
Now I need to finish Rcov for this controller so I need to add/modify spec to cover both 'some_condition' results. and I want to validate 'lelf_panel' locals passed to render, as if I only validate the render_template, partial page rendered for both result are the same.
I check the 'render_template' in rspec docs in
http://rubydoc.info/gems/rspec-rails/2.8.1/RSpec/Rails/Matchers/RenderTemplate:render_template
it only provide and 2nd params for message, so how can I test the locals passed to render?
Instead of using the render_template matcher, you can use an expectation on the controller object.
it 'should render correct template for lefl_panel' do
# ...
allow(controller).to receive(:render).with no_args
expect(controller).to receive(:render).with({
:partial => 'system/_left_panel',
:layout => false,
:locals => some_very_long_hash_A
})
get 'left_panel'
end
Same as #ryan-ahearn 's answer with suggestions from #user2490003 's comment - but all put into something more flexible and for RSpec 3.
# Safe to set globally, since actions can either render or redirect once or fail anyway
before do
allow(controller).to receive(:render).and_call_original
end
describe "get left panel" do
before do
# other setup
get 'left_panel'
end
it 'should render correct template for lelf_panel' do
# Sadly, render_template is primitive (no hash_including, no block with args, etc.)
expect(subject).to render_template('system/_left_panel')
end
it 'should render with correct local value' do
expect(controller).to have_received(:render) do |options|
expect(options[:locals][:key_from_very_long_hash]).to eq('value_of_key_from_very_long_hash')
end
end
end
as far as I know, there is no way to directly examine the locals for a template in the way you're describing.
You could change locals_hash to #locals_hash and then examine the results through assigns( :locals_hash).
Or, you could use selectors on the resulting HTML and check that some indicative content is there -- for instance, if locals_hash affects the title of the page, check that the resulting HTML page title is what you expect.

Resources