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
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.
Capybara and RSpec can be categorized as "Testing Frameworks" tools. Capybara and RSpec are both open source tools. It seems that Capybara with 8.85K GitHub stars and 1.29K forks on GitHub has more adoption than RSpec with 2.53K GitHub stars and 202 GitHub forks.
Use the allow method with the receive matcher on a test double or a real. object to tell the object to return a value (or values) in response to a given. message. Nothing happens if the message is never received.
The describe Keyword The word describe is an RSpec keyword. It is used to define an “Example Group”. You can think of an “Example Group” as a collection of tests. The describe keyword can take a class name and/or string argument.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With