I'm looking for a clean way to use JBuilder and test the json output with RSpec. The popular way for JSON testing is to implement the as_json method, and then in RSpec compare the received object with the object.to_json method. But a large reason I'm using JBuilder is that I don't want all the attributes that to_json spits out; so this breaks comparison.
Currently with JBuilder I'm having to do the following to test the RSpec results:
1) Create a Factory object: #venue
2) Create a hash inside my RSpec test that contains the "expected" JSON string back
#expected => {:id => #venue.id,:name=>#venue.name..........}
2) Compare the #expected string to the results.response.body that is returned from the JSON call.
This seems simple, except I have objects being rendered with 15+ attributes, and building the #expected hash string is cumbersome and very brittle. Is there a better way to do this?
You should be able to test your Jbuilder views with RSpec views specs. You can see the docs at https://www.relishapp.com/rspec/rspec-rails/v/2-13/docs/view-specs/view-spec.
An example spec for a file located at 'app/views/api/users/_user.json.jbuilder', could be something like this (spec/views/api/users/_user.json.jbuilder_spec.rb):
require 'spec_helper'
describe 'user rendering' do
let(:current_user) { User.new(id: 1, email: 'foo#bar.com') }
before do
view.stub(:current_user).and_return(current_user)
end
it 'does something' do
render 'api/users/user', user: current_user
expect(rendered).to match('foo#bar.com')
end
end
I don't like testing the JSON API through the views, because you have to essentially mimic, in the test, the setup already done in the controller. Also, bypassing the controller, you aren't really testing the API.
In controller tests, however, you'll find that you don't get any JSON returned in the response body. The response body is empty. This is because RSpec disables view rendering in controller tests. (For better or worse.)
In order to have an RSpec controller test of your view rendered JSON API, you must add the render_views directive at the top of your test. See this blog post (not mine), for more detailed information about using RSpec controller tests with Jbuilder.
Also, see this answer.
I have not been able to make RSpec work with the views yet, but I am testing my JSON API via controller RSpec tests. To assist with this process, I am using the api matchers gem. This gem lets you construct RSpec tests such as:
it "should have a 200 code" do
get :list, :format => :json
response.should be_success
response.body.should have_json_node( :code ).with( "200" )
response.body.should have_json_node( :msg ).with( "parameter missing" )
end
This sounds like a good use case for RSpec view specs. Are you using JBuilder for the output of a controller in views?
For example, in spec/views/venues_spec.rb
require 'spec_helper'
describe "venues/show" do
it "renders venue json" do
venue = FactoryGirl.create(:venue)
assign(:venue, venue])
render
expect(view).to render_template(:partial => "_venue")
venue_hash = JSON.parse(rendered)
venue_hash['id'].should == #venue.id
end
end
It's a little clunkier than with say ERB, but you can use binding and eval to run the Jbuilder template directly. E.g. given a typical Jbuilder template app/views/items/_item.json.jbuilder that refers to an instance item of the Item model:
json.extract! item, :id, :name, :active, :created_at, :updated_at
json.url item_url(item, format: :json)
Say you have an endpoint that returns a single Item object. In your request spec, you hit that endpoint:
get item_url(id: 1), as: :json
expect(response).to be_successful # just to be sure
To get the expected value, you can evaluate the template as follows:
item = Item.find(1) # local variable `item` needed by template
json = JbuilderTemplate.new(JbuilderHandler) # local variable `json`, ditto
template_path = 'app/views/items/_item.json.jbuilder'
binding.eval(File.read(template_path)) # run the template
# or, for better error messages:
# binding.eval(File.read(template_path), File.basename(template_path))
expected_json = json.target! # template result, as a string
Then you can compare the template output to your raw HTTP response:
expect(response.body).to eq(expected_json) # plain string comparison
Or, of course, you can parse and compare the parsed results:
actual_value = JSON.parse(response.body)
expected_value = JSON.parse(expected_json)
expect(actual_value).to eq(expected_value)
If you're going to be doing this a lot -- or if, for instance, you want to be able to compare the template result against individual elements of a returned JSON array, you might want to extract a method:
def template_result(template_path, bind)
json = JbuilderTemplate.new(JbuilderHandler)
# `bind` is passed in and doesn't include locals declared here,
# so we need to add `json` explicitly
bind.local_variable_set(:json, json)
bind.eval(File.read(template_path), File.basename(template_path))
JSON.parse(json.target!)
end
You can then do things like:
it 'sorts by name by default' do
get items_url, as: :json
expect(response).to be_successful
parsed_response = JSON.parse(response.body)
expect(parsed_response.size).to eq(Item.count)
expected_items = Item.order(:name)
expected_items.each_with_index do |item, i| # item is used by `binding`
expected_json = template_result('app/views/items/_item.json.jbuilder', binding)
expect(parsed_response[i]).to eq(expected_json)
end
end
You can call the render function directly.
This was key for me to get local variables to work.
require "rails_helper"
RSpec.describe "api/r2/meditations/_meditation", type: :view do
it "renders json" do
meditation = create(:meditation)
render partial: "api/r2/meditations/meditation", locals: {meditation: meditation}
meditation_hash = JSON.parse(rendered)
expect(meditation_hash['slug']).to eq meditation.slug
expect(meditation_hash['description']).to eq meditation.description
end
end
Related
I have a controller spec for my application, which tests the create method on a controller. The create action actually works fine, but the spec is failing. It seems that it is auto converting the the hash POST param into a string.
let(:coupon) { attributes_for(:coupon) }
describe 'POST #create' do
it 'should create a new coupon from params' do
expect {
post :create, :coupon => coupon
}.to change(Coupon, :count).by(1)
end
end
Now, If I do puts coupon it is generating a valid hash of data, and the type is hash. For some reason the controller is receiving a string for params[:coupon]. Only in rspec testing does this happen, when I test in the browse with a POST form it works perfectly fine.
Rspec throws the following message:
NoMethodError:
undefined method `permit' for #<String:0x00000005062700>
Did you mean? print
and if I do puts params[:coupon].class in the controller in rspec it gives me String. Why might it be converting my hash into a string for the POST request, and how can I prevent this ?
I am using Rails 5.0.0 and rspec 3.5.1
This exact same behavior showed up for me recently when testing a JSON API endpoint. Originally I had this as my subject:
subject { put :my_endpoint, **input_args }
and an integer value in input_args was getting translated into a string. The fix was to add format: 'json' as an additional keyword argument to put:
subject { put :my_endpoint, **input_args, format: 'json' }
It seems that it was an issue with the gem open_taobao somehow transforming my post requests in tests.
Shouldn't I be able to see instance variables which are created in a controller action from within my rspect tests?
# /app/controllers/widget_controller.rb
...
def show
#widget = ...
puts "in controller: #{#widget}"
end
...
--
# /spec/controllers/widget_controller_spec.rb
RSpec.describe WidgetController, type: :controller do
...
describe "GET #show" do
it "assigns the requested widget as #widget" do
get :show, { :id => 1 } # this is just an example - I'm not hardcoding the id
puts "in spec: #{#widget}"
end
end
...
Here is the output I get when I run that spec:
controller: #<Widget:0x007f9d02aff090>
in spec:
Am I wrong in thinking that I should have access to #widget in my controller spec?
Use the assigns method (*note: assigns is now deprecated. See bottom of my answer for info):
it "assigns the #widget"
expect(assigns(:widget)).not_to be_nil
end
Of course, you can inspect widget as you'd like, but without seeing what #widget is from your provided controller code, I just checked if it was nil
If you're wanting to puts the widget, like in your example, just print it with assigns
puts assigns(:widget)
EDIT: assigns is now deprecated (see: https://apidock.com/rails/ActionController/TestProcess/assigns)
If you want to continue using assigns you will need to install the rails-controller-testing gem.
Otherwise, you could use what rails-controller-testing gem uses internally: #controller.view_assigns[]
#assigns is deprecated. Here's a solution that avoids pulling in the rails-controller-testing gem
Though it can be a code smell to test instance variables in controllers, to upgrade older apps you can leverage #instance_variable_get.
Rspec example:
expect(#controller.instance_variable_get(:#person).id).to eq(person_id)
In Rails 5, assigns has now been split out into a new rails-controller-testing gem.
You can either install the gem to continue using assigns, or you could use what the rails-controller-testing gem uses internally, which#controller.view_assigns[].
In new Rails versions with rails-controller-testing gem you can access to instance variables two ways:
with assigns[:key] / assigns['key']
with controller.view_assigns['key']
As you see assigns is ActiveSupport::HashWithIndifferentAccess, so you you can use both a string or symbol as key. It is formed with regular_writer method from the Hash, where all the keys with instance variables are strings, i.e. from #controller.view_assigns. But you can also access to controller inside tests
Here example
require 'rails_helper'
describe SomeController, type: :controller do
before do
assign(:some_var, 10)
get :show, params: { id: 1 }
end
it 'assigns the #some_var'
expect(assigns['some_var']).to eq 10
expect(assigns[:some_var]).to eq 10
expect(controller.view_assigns['some_var']).to eq 10
end
end
I have a Controller:
class ThingController < ActionController
respond_to :json
def create
puts "CREATE " + params.inspect
end
end
and a test:
require "spec_helper"
describe "/thing" do
context "create" do
it "should get params" do
params = {"a" => "b", "c" => ["d"], "e" => [], "f"=>"",
"g"=>nil, , "controller" => "NOPE", "action" => "NOPE"}
post uri, params
end
end
end
When I run this, the following is logged:
CREATE {"a"=>"b", "c"=>["d"], "action"=>"create", "controller"=>"thing"}
My questions are:
where did e go? I would expect it to deserialize to an empty array, not to nothing at all.
why are the action and controller params being mixed into this? Aren't the body and the rails internals completely separate concerns?
Because of this, my action and controller JSON fields were over-written. How would I access these?
Is this therefore not the right way to accept JSON?
I'm new to Rails, but I have done a lot of Django.
There are two parts to this problem: you need to ensure that your parameters are being sent as JSON, and also that they are being interpreted as JSON.
Essentially, you have to
encode your parameters as JSON
set appropriate content-type and accepts headers
See POSTing raw JSON data with Rails 3.2.11 and RSpec for the way.
The rails middleware will add the action and controller params so you'll have to put those in a nested hash if you still want to access your custom values.
Try adding format: 'json' to the params in your test. This will send a different content-type header and might help serialize the params correctly in order to keep the e param.
I'm trying to set the header for some RSpec requests that require authentication. The header is ACCESS_TOKEN. No matter how I attempt to set the header, it never gets set. I know the app works because I can manually test it, I just cant get rspec tests to work. See the full source code & tests for this problem here: https://github.com/lightswitch05/rspec-set-header-example
Since authentication is used in most of my request specs, I've created support helper module to retrieve an access token and set it in the header. Below is the summary of how I'm trying to set the header, see everything I've tried in the full source
# my_app/spec/support/session_helper.rb
module SessionHelper
def retrieve_access_token
post api_v1_session_path({email: 'test#example.com', password: 'poor_password'})
expect(response.response_code).to eq 201
expect(response.body).to match(/"access_token":".{20}"/)
parsed = JSON(response.body)
token = parsed['access_token']['access_token']
#request.headers['HTTP_ACCESS_TOKEN'] = token
end
end
an example request spec that uses this helper and should work, but always fails because the header never gets set:
# my_app/spec/requests/posts_spec.rb
# ...
context "create" do
it "creates a post" do
retrieve_access_token
post = FactoryGirl.build(:post)
post api_v1_posts_path(
post: {
title: post.title,
content: post.content
}
)
expect(response.body).to include('"id":')
expect(response.body).to include('"title":"' + post.title + '"')
expect(response.body).to include('"content":"' + post.content + '"')
expect(response.response_code).to eq 201
end
end
I know I can manually set the header in the individual get and post requests - but that is not a maintainable solution for API-wide authorization. Imagine having to change every test if the header name changed slightly.
Note: This answer is based on what you seem to be calling api_v1_session_path with post request to SessionsController for every spec you're trying to run in your requests specs.
There are two ways to solve the issue I figured you have here.
Solution #1 - Either you create another helper method in your SessionHelper or in some other helper file called support/requests_helper.rb(however you prefer). I'd create another helper in support/requests_helper.rb:
module RequestsHelper
def get_with_token(path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
get path, params, headers
end
def post_with_token(path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
post path, params, headers
end
# similarly for xhr..
end
then in rails_helper.rb:
# Include the sessions helper
config.include SessionHelper, type: :request
# Include the requests helper
config.include RequestsHelper, type: :request
change session_helper.rb:
# my_app/spec/support/session_helper.rb
module SessionHelper
def retrieve_access_token
post api_v1_session_path({email: 'test#example.com', password: 'poor_password'})
expect(response.response_code).to eq 201
expect(response.body).to match(/"access_token":".{20}"/)
parsed = JSON(response.body)
parsed['access_token']['access_token'] # return token here!!
end
end
Now, you can change your all requests specs like this:
describe Api::V1::PostsController do
context "index" do
it "retrieves the posts" do
get_with_token api_v1_posts_path
expect(response.body).to include('"posts":[]')
expect(response.response_code).to eq 200
end
it "requires a valid session key" do
get api_v1_posts_path
expect(response.body).to include('"error":"unauthenticated"')
expect(response.response_code).to eq 401
end
end
end
Solution #2 - Change specs/factories/access_token_factory.rb to:
FactoryGirl.define do
factory :access_token do
active true
end
# can be used when you want to test against expired access tokens:
factory :inactive_access_token do
active false
end
end
Now, change your all requests specs to use access_token:
describe Api::V1::PostsController do
context "index" do
let(:access_token){ FactoryGirl.create(:access_token) }
it "retrieves the posts" do
# You will have to send HEADERS while making request like this:
get api_v1_posts_path, nil, { 'HTTP_ACCESS_TOKEN' => access_token.access_token }
expect(response.body).to include('"posts":[]')
expect(response.response_code).to eq 200
end
it "requires a valid session key" do
get api_v1_posts_path
expect(response.body).to include('"error":"unauthenticated"')
expect(response.response_code).to eq 401
end
end
end
I'd go with "Solution #1" as it removes a burden of making you remember to send HTTP_ACCESS_TOKEN in headers every time you want to make such requests.
Common misconception is to treat controller and request tests equally.
It would be good to start from reading about controller specs and request specs. As you can see, controller specs simulate http request, while request specs perform full stack request.
You can find some good article about why you should write controller specs and what to test there here. While it is good to write them, they shouldn't be touching database in my opinion.
So while Voxdei answer is partially valid (after changing request specs to controller specs your way of setting headers will work), it misses the point in my opinion.
In request specs, you cannot just use request / controller methods, you have to pass your headers in hash as third argument of your request methods, so i.e.
post '/something', {}, {'MY-HEADER' => 'value'}
What you could do though is to stub authentication like:
before do
allow(AccessToken).to receive("authenticate").and_return(true)
end
Then you could test your authentication in one spec to be sure that it works and use such before filter in other specs. This is also probably better approach as performing additional request every time you run spec needing authentication is quite huge overhead.
I also found quite interesting pull request in grape gem which tries to add default headers behaviour so you could also try with such approach if you would really want to use default headers in request specs.
Probably because of how now Rspec treats spec files. It no longer automatically infers spec type from a file location
Try either setting this behavior back to what you used to know
RSpec.configure do |config|
config.infer_spec_type_from_file_location!
end
or set it locally for each controller spec files in your project
describe MyController, type: :controller do
# your specs accessing #request
end
Surya's answer is the best. But you can DRY it up a little bit more:
def request_with_user_session(method, path, params={}, headers={})
headers.merge!('HTTP_ACCESS_TOKEN' => retrieve_access_token)
send(method, path, params, headers)
end
Here you have only one method and call the request method by the given parameter method.
I stub the function that authenticates the request to return true or any value returned by the function.
ApplicationController.any_instance.stub(:authenticate_request) { true }
I have a function that sends a variable to js with the help of gon.
def calc_foo
# calculate foo
gon.foo = foo
end
I want to test this function i.e make sure that the method return the correct value, using rspec.
it "should return bar" do
foo = #foo_controller.calc_foo
expect(foo).to eq(bar)
end
But, I get the following error message when the test case reaches the line where the variable is sent to gon.
Failure/Error: foo = #foo_controller.calc_foo
NoMethodError:
undefined method `uuid' for nil:NilClass
I have checked the value for foo, and it is not Nil, so gon must be Nil.
I believe the error is that I don't incude gon correctly. This is the rspec-part of my Gemfile
#rspec-rails includes RSpec itself in a wrapper to make it play nicely with Rails.
#Factory_girl replaces Rails’ default fixtures for feeding test data
#to the test suite with much more preferable factories.
group :development, :test do
gem 'rspec-rails'
gem 'factory_girl_rails'
gem 'capybara'
gem 'gon'
end
So how can I get rspec to play nicely with gon?
(I have also tried to include gon in my spec-file with no success)
In the controller specs (where gon belongs) you'll need to make an actual request to bypass your problem:
RSpec.describe ThingiesController do
let(:gon) { RequestStore.store[:gon].gon }
describe 'GET #new' do
it 'gonifies as expected' do
get :new, {}, valid_session # <= this one
expect(gon['key']).to eq :value
end
end
end
If you'd rather not stick with certain controller or action for the gon-related specs (let's say you have a gon-related method in your ApplicationController), you could use Anonymous controller approach:
RSpec.describe ApplicationController do
let(:gon) { RequestStore.store[:gon].gon }
controller do
# # Feel free to specify any of the required callbacks,
# # like
# skip_authorization_check
# # (if you use `CanCan`)
# # or
# before_action :required_callback_name
def index
render text: :whatever
end
end
describe '#gon_related_method' do
it 'gonifies as expected' do
get :index
expect(gon['key']).to eq :value
end
end
end
I have lots of controller and request/integration specs and can confirm that gon behaves fine there as long as you make actual requests.
However, I still have a problem similar to yours although the case is different (making requests from the shared_examples included in the controller specs). I've opened the relevant issue, feel free to join the conversation (for anyone interested).
I test that the controller passes the right stuff to gon in a request spec.
The controller sets an array of objects -- e.g., gon.map_markers = [...]
My request spec extracts the JSON via regexp (the .split() and match_array handle the order-independent-array-ness):
....
# match something like
# gon.map_markers=[{"lat":a,"lng":b},{"lat":c,"lng":d},...];
# and reduce/convert it to
# ['"lat":a,"lng":b',
# '"lat":c,"lng":d',
# ...
# ]
actual_map_markers = response.body
.match('gon.map_markers=\[\{([^\]]*)\}\]')[1]
.split('},{')
expect(actual_map_markers).to match_array expected_map_markers