I have a test that checks if the requested page returns a 200 status code:
expect(response).to have_http_status(:success)
However, if I explicitly return a different status code using the following the test:
return render json: { error: 'error message' }, status: :unprocessable_entity
it still passes.
Why do response and last_response have different statuses:
response.status # 200
last_response.status # 422
response is provided by ActionController::TestCase.
From the docs:
An ActionDispatch::TestResponse object, representing the response of the last HTTP response.
For reference, here are the rspec docs for controller tests. This may help clear up how response is supposed to be used.
last_response comes from Rack::MockResponse < Rack::Response
From the docs:
Return the last response received in the session. Raises an error if no requests have been sent yet.
In your test case, you probably used a method that allows you to mock visiting a page. This will set your response.status to 200 as you've had a successful request. If you then use Rack to stimulate an endpoint, e.g.:
put '/users', {my_user: 'blah'}
and you do it with incorrect parameters, then your last_response.status will be 422.
Ultimately, the confusion comes down the similarity of naming between ActionController and Rack::MockResponse, which I agree is rather confusing.
Related
I need to solve a test of my app. Coverage is complaining about a line of code that evaluates the connection to MongoDB (rescue Mongo::Error::NoServerAvailable => _e) and renders the error.
What do you think I should use to test this:
def index
render json: Complex.all.to_json
rescue Mongo::Error::NoServerAvailable => _e
render json: { error_description: 'no database available' }, status: 503
end
I am trying to test with something like:
it 'should return an exception' do
get :index
expect(response).to raise_exception
end
I found that I should use
.and_raise(IOError)
But I am not sure where to use it to make it fall on the line.
Actually I can make it fall on the exception if I stop Mongo, but that's not the idea.
Thanks for your time.
To reach the line of code that handles the exception, stub Complex.all.to_json to raise the exception. Since Complex.all.json is chained it takes a little extra effort to stub it. Also, since the exception is handled, you can't test that it's raised; instead, test the result of handling it.
it 'should handle the exception' do
all = double
allow(all).to receive(:to_json).and_raise Mongo::Error::NoServerAvailable
allow(Complex).to receive(:all).and_return all
get :index
expect(response.status).to eq(503)
expect(response.body).to include('no database available')
# you could test the JSON more thoroughly, but you get the idea
end
You could use receive_message_chain to stub Complex.all.to_json with less code. I used the long version since it's easier to understand what's going on.
In a Rails 4.2.0 application tested with rspec-rails, I provide a JSON web API with a REST-like resource with a mandatory attribute mand_attr.
I'd like to test that this API answers with HTTP code 400 (BAD REQUEST) when that attribute is missing from a POST request. (See second example blow.) My controller tries to cause this HTTP code by throwing an ActionController::ParameterMissing, as illustrated by the first RSpec example below.
In other RSpec examples, I want raised exceptions to be rescued by the examples (if they're expected) or to hit the test runner, so they're displayed to the developer (if the error is unexpected), thus I do not want to remove
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
from config/environments/test.rb.
My plan was to have something like the following in a request spec:
describe 'POST' do
let(:perform_request) { post '/my/api/my_ressource', request_body, request_header }
let(:request_header) { { 'CONTENT_TYPE' => 'application/json' } }
context 'without mandatory attribute' do
let(:request_body) do
{}.to_json
end
it 'raises a ParameterMissing error' do
expect { perform_request }.to raise_error ActionController::ParameterMissing,
'param is missing or the value is empty: mand_attr'
end
context 'in production' do
###############################################################
# How do I make this work without breaking the example above? #
###############################################################
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
# Above matcher provided by api-matchers. Expectation equivalent to
# expect(response.status).to eq 400
end
end
end
# Below are the examples for the happy path.
# They're not relevant to this question, but I thought
# I'd let you see them for context and illustration.
context 'with mandatory attribute' do
let(:request_body) do
{ mand_attr: 'something' }.to_json
end
it 'creates a ressource entry' do
expect { perform_request }.to change(MyRessource, :count).by 1
end
it 'reports that a ressource entry was created (HTTP status 201)' do
perform_request
expect(response).to create_resource
# Above matcher provided by api-matchers. Expectation equivalent to
# expect(response.status).to eq 201
end
end
end
I have found two working and one partially working solutions which I'll post as answers. But I'm not particularly happy with any of them, so if you can come up with something better (or just different), I'd like to see your approach! Also, if a request spec is the wrong type of spec to test this, I'd like to know so.
I foresee the question
Why are you testing the Rails framework instead of just your Rails application? The Rails framework has tests of its own!
so let me answer that pre-emptively: I feel I'm not testing the framework itself here, but whether I'm using the framework correctly. My controller doesn't inherit from ActionController::Base but from ActionController::API and I didn't know whether ActionController::API uses ActionDispatch::ExceptionWrapper by default or whether I first would have had to tell my controller to do so somehow.
You'd want to use RSpec filters for that. If you do it this way, the modification to Rails.application.config.action_dispatch.show_exceptions will be local to the example and not interfere with your other tests:
# This configure block can be moved into a spec helper
RSpec.configure do |config|
config.before(:example, exceptions: :catch) do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions) { true }
end
end
RSpec.describe 'POST' do
let(:perform_request) { post '/my/api/my_ressource', request_body }
context 'without mandatory attribute' do
let(:request_body) do
{}.to_json
end
it 'raises a ParameterMissing error' do
expect { perform_request }.to raise_error ActionController::ParameterMissing
end
context 'in production', exceptions: :catch do
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
end
end
The exceptions: :catch is "arbitrary metadata" in RSpec speak, I chose the naming here for readability.
Returning nil from a partially mocked application config with
context 'in production' do
before do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions)
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
or more explicitly with
context 'in production' do
before do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions).and_return nil
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
would work if that was the only example being run. But if it was, we could just as well drop the setting from config/environments/test.rb, so this is a bit moot. When there are several examples, this will not work, as Rails.application.env_config(), which queries this setting, caches its result.
Mocking Rails.application.env_config() to return a modified result
context 'in production' do
before do
# We don't really want to test in a production environment,
# just in a slightly deviating test environment,
# so use the current test environment as a starting point ...
pseudo_production_config = Rails.application.env_config.clone
# ... and just remove the one test-specific setting we don't want here:
pseudo_production_config.delete 'action_dispatch.show_exceptions'
# Then let `Rails.application.env_config()` return that modified Hash
# for subsequent calls within this RSpec context.
allow(Rails.application).to receive(:env_config).
and_return pseudo_production_config
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
will do the trick. Note that we clone the result from env_config(), lest we modify the original Hash which would affect all examples.
context 'in production' do
around do |example|
# Run examples without the setting:
show_exceptions = Rails.application.env_config.delete 'action_dispatch.show_exceptions'
example.run
# Restore the setting:
Rails.application.env_config['action_dispatch.show_exceptions'] = show_exceptions
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
will do the trick, but feels kinda dirty. It works because Rails.application.env_config() gives access to the underlying Hash it uses for caching its result, so we can directly modify that.
In my opinion the exception test does not belong in a request spec; request specs are generally to test your api from the client's perspective to make sure your whole application stack is working as expected. They are also similar in nature to feature tests when testing a user interface. So because your clients won't be seeing this exception, it probably does not belong there.
I can also sympathize with your concern about using the framework correctly and wanting to make sure of that, but it does seem like you are getting too involved with the inner workings of the framework.
What I would do is first figure out whether I am using the feature in the framework correctly, (this can be done with a TDD approach or as a spike); once I understand how to accomplish what I want to accomplish, I'd write a request spec where I take the role of a client, and not worry about the framework details; just test the output given specific inputs.
I'd be interested to see the code that you have written in your controller because this can also be used to determine the testing strategy. If you wrote the code that raises the exception then that might justify a test for it, but ideally this would be a unit test for the controller. Which would be a controller test in an rspec-rails environment.
I am trying to test receive JSON webhooks from Stripe.
I have read:
Stripe Webhook on Rails
https://stripe.com/docs/webhooks
They require a 200 status response in order to acknowledge receipt.
I want to solve this before moving on to dealing with the JSON.
routes
post 'webhook' => 'web_hook#webhook'
controller
Stripe.api_key = "sk_test_whatsupbuttercup"
class WebHookController < ApplicationController
protect_from_forgery :except => :webhook
def webhook
render status: 200
end
end
With this setup, when I test a webhook, Stripe receives a 500 error.
If you only want to return a status use
head :ok
Instead of render. :ok is the corresponding symbol for 200 but you can also use it with the status code itself.
head 200
A full list of codes and corresponding symbols can be found here...
http://guides.rubyonrails.org/layouts_and_rendering.html
Whenever you get a 500 error (or any time you're confused about how your app is behaving actually) you should look in your logs. In this case you'll probably find that there's an ActionView::MissingTemplate error because you're rendering but not including anything to render.
redirect_to browse_path(asset.parent_id), notice: "successfully created file!", status: 201
201 is the status you should set if you've created a resource. While the above method works for a create action, the spec for its action no longer does:
subject { response }
describe '.create' do
context 'when orphan' do
before do
post :create, asset: { parent_id: nil, uploaded_file: file }
end
it { should have_http_status 201 }
it { should redirect_to '/' }
end
end
The status expectation passes, but the redirect_to expectation fails:
Expected response to be a <redirect>, but was <201>
I accept that it's no longer a 302 redirect, but it does still redirect the user to a new route (which I want to test). The redirect_to spec passes if I set it to the "wrong" code of 302, rather than 201:
redirect_to browse_path(asset.parent_id), notice: "successfully created file!", status: 302
so should I bother with setting status codes? I'll admit I actually have no idea how the browser uses them and my applications functions just as well if I painstakingly set them in my actions or don't (just use 302 redirects and 200 successes).
If status codes are important, how should I get my above specs to pass?
From the docs:
The status code can either be a standard HTTP Status code as an
integer, or a symbol representing the downcased, underscored and
symbolized description. Note that the status code must be a 3xx HTTP
code, or redirection will not occur.
(emphasis added)
Simply put, it is an error to redirect with a 201 status code.
You can assert off response.body or other response attributes within rspec. In this case the thing you are after is response.header["Location"]
You can choose to dodge the problem with capybara/rspec where you can assert current_url and still assert the status code.
redirect_to is just a dumb mid level helper and you need to reach to a slightly lower level in response.something or higher level with capybara to get where you want to be.
One way is this:
its(:status){ should eq 201 }
its(:location){ should eq 'http://test.host/' }
When Rails responds to an HTTP request, it's HTTP response will always be correctly headed up with an appropriate HTTP response code. This can be for successful operations (a 2xx code), such as the creation of a new ActiveRecord in the database, or for errors (a 4xx). In the latter case a rendered HTML page is supplied containing information about the error (a backtrace, etc).
My app requires that all Rails HTTP responses take the form of JSON, so I am writing my own code to render these HTTP responses accordingly. A number of tutorials talk about writing something like this to render such responses (in this example, located in user_account_controller.rb where UserAccount is the name of an ActiveRecord model):
# method to create a UserAccount object based on supplied user_account_params
def create
#user_account = UserAccount.create!(user_account_params)
if #user_account
render :status => 201, :json => {
'message' : 'Operation was successful!"
}
end
end
And, if an exception is thrown, it is possible to bind a customer exception handler as follows:
# In ApplicationController
rescue_from Exception, :with => :json_exception_handler
def json_exception_handler(exception)
render :status => 422, :json => {
:exception => {
:backtrace => exception.backtrace,
:message => exception.message
}
}
end
The problem with both of these solutions is that they require me to statically set the HTTP response codes to have the same value every time (201 and 422 in these examples). Rails already does a great job of automatically picking the best response code to use, depending on the type of error or type of successful operation. I don't want to have to reinvent the wheel badly by inventing my own response code structure for each error and operation in my app.
So my question is this: how do I supply the response code that Rails will have automatically chosen anyway, whilst retaining the ability to print out custom JSON?