Testing a Rails API With RSpec
As I continue to improve me API testing skills in Rails, I have come to point where I really feel comfortable with all the tools I need to correctly add useful tests to my API. This post explains how I usually test my Rails APIs using RSpec.
Setting Up the Necessary Tools
In addition to RSpec, there are a few tools that make my testing experience much easier. These are:
After Adding these gems to the Gemfile. I proceed to create a /support
directory under the /spec
directory. This directory will contain the additional configuration of the above tools, as well as some helper definitions that our specs will use.
/spec/support/shoulda.rb:
Shoulda::Matchers.configure do |config|
config.integrate do |with|
# Choose a test framework:
with.test_framework :rspec
# Or, choose the following (which implies all of the above):
with.library :rails
end
end
/spec/support/database_cleaner.rb:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
It will also be necessary that we make RSpec Rails recognize the above configurations:
/spec/rails_helper.rb:
# Add additional requires below this line. Rails is not loaded until this point!
require 'shoulda/matchers'
require 'support/shoulda'
require 'support/database_cleaner'
Perfect. With this setup we are ready to dive into the specs.
Model Specs
Model specs in a Rails API don't really differ much from the model specs of a typical Rails application. Still, I am including here for completeness. Notice how shoulda_matchers
greatly reduce the code in the specs, and improves readability:
/spec/models/user_spec.rb:
require 'rails_helper'
RSpec.describe User, type: :model do
it 'has a valid factory' do
expect(FactoryGirl.create(:user)).to be_valid
end
context 'validations' do
it { is_expected.to validate_presence_of :email }
it { is_expected.to validate_uniqueness_of :email }
it { is_expected.to validate_confirmation_of :password }
it { is_expected.to validate_presence_of :first_name }
it { is_expected.to validate_presence_of :last_name }
end
end
Routing Specs
You should not really bother creating tests for REST routes in a Rails API. A Rails --api
application will automatically not include new
and edit
routes in your routes configuration. So there is no need to test that these routes are effectively not routable.
However, this is a good place to test additional non REST-ful routes. For example, an authentication endpoint that returns a JSON Web Token (JWT):
/spec/routing/authentication_routing_spec.rb:
require 'rails_helper'
RSpec.describe Api::V1::AuthenticationController, type: :routing do
describe 'authentication routing' do
it 'routes to /v1/auth to user_token#create' do
expect(:post => '/v1/auth').to route_to('api/v1/user_token#create')
end
end
end
In the example above I am using the Knock gem to easily implement JWT authentication in the application.
Request Specs
In my Rails APIs, controller specs are completely replaced by request specs. This is because request specs directly hit the API endpoints and simulates how users would actually interact with the API, without worrying about the controller behavior.
Let's take a look at an example spec for a GET /users
request:
/spec/requests/users_spec.rb:
describe 'GET /v1/users' do
let!(:users) { FactoryGirl.create_list(:user, 10) }
before { get '/v1/users', headers: { 'Accept': 'application/vnd' } }
it 'returns HTTP status 200' do
expect(response).to have_http_status 200
end
it 'returns all users' do
body = JSON.parse(response.body)
expect(body['data'].size).to eq(10)
end
end
I've seen many request spec examples use multiple expectations for each example. I prefer creating multiple examples with only one expectation for each. In the example above, we have two examples.
Additionally, the actual request is done in a before
block so that we can separate the expectations accross multiple examples. This will slow down the test suite a bit, however.
DRY-ing The JSON Response
In request specs, it is very common to see the response being parsed and asserted like this:
body = JSON.parse(response.body)
user_email = body['data']['attributes']['email']
expect(user_email).to eq 'pabloescobar@domain.com'
Writing each example that way can get tiring really fast. We can nicely DRY it up by creating a helper method in a new module. This can be created in the /support
directory:
/spec/support/request_helpers.rb:
module Request
module JsonHelpers
def json_response
@json_response ||= JSON.parse(response.body, symbolize_names: true)
end
end
end
Then include it in the RSpec configuration:
/spec/rails_helper.rb:
require 'support/request_helpers'
# ...
config.include Request::JsonHelpers, type: :request
Our helper provides a json_response
method that will return the parsed response. It can also take an option symbolize_names
if you want to use symbols instead of strings or vice-versa. Now we can re-write the examples like this (using symbols):
it 'returns all users' do
expect(json_response[:data].size).to eq(10)
end
it 'returns the requested user' do
expect(json_response[:data][:attributes][:email]).to eq('james@text.com')
end
Request Headers
It is important to include the correct headers in our request specs. Otherwise you could be seeing mischievous failing tests. When using the JSON API specification, I use the following headers for GET
requests:
before { get '/v1/users', headers: { 'Accept': 'application/vnd' } }
For POST
, PUT
, and PATCH
:
post '/v1/users', params: new_user.to_json, headers: { 'Accept': 'application/vnd', 'Content-Type': 'application/vnd.api+json' }
JWT Authentication Helper
When using the Knock gem, a JWT token must be passed via the request headers when requesting endpoints that require authentication. Like this:
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
GET /my_resources
We can create a helper that returns this additional header along with the token, so that it can be used in the request specs. We can add a new module to the Request
module we just created in /spec/support/request_helpers.rb
:
/spec/support/request_helpers.rb:
module Request
# ...
module AuthHelpers
def auth_headers(user)
token = Knock::AuthToken.new(payload: { sub: user.id }).token
{
'Authorization': "Bearer #{token}"
}
end
end
end
Likewise, we must include it in rails_helper.rb
:
# ...
config.include Request::AuthHelpers, type: :request
Then in our request specs, we can use it in the following way:
describe 'GET /v1/users' do
let!(:users) { FactoryGirl.create_list(:user, 10) }
before { get '/v1/users', headers: auth_headers(current_user) }
it 'returns HTTP status 200' do
expect(response).to have_http_status 200
end
end
Of course, you will also have to add the other headers I previously mentioned.
Conclusion
And there it is. Simple yet effective tests that should give you confidence that your API is working the way you expect it to.
I hope I can update this article in the future and include other topics such as more complex Factory Girl factories, among other things.