Rails API Testing Best Practices

Writing an API is almost a given with modern web applications. I’d like to lay out some simple guidelines and best practises for Rails API testing. We need to determine what to test and why it should be tested. Once we’ve established what we will be writing tests for, we will set up RSpec to do this quickly and easily. Basically we’ll be sending HTTP requests and testing that the response status codes and content match our expectations.

What to test?

A properly designed API should return two things: an HTTP response status-code and the response body. Testing the status-code is necessary for web applications with user authentication and resources with different permissions. That being said, testing the response body should just verify that the application is sending the right content.

HTTP Status Codes

Typical HTTP responses for a simple API on an application with authentication will generally fall within the following 4 status codes:

  • 200: OK – Basically self-explanitory, the request went okay.
  • 401: Unauthorized – Authentication credentials were invalid.
  • 403: Forbidden – The resource requested is not accessible – in a Rails app, this would generally be based on permissions.
  • 404: Not Found – The resource doesn’t exist on the server.

If you’re wondering why not just use 401 – Unauthorized or 403 – Forbidden for every permission/auth error, I’d suggest reading this stackoverflow answer. If that’s not enough, check out the W3 spec.

Response Body

It goes without saying that the content body should contain the resources that you requested and shouldn’t contain attributes that are private. This is straight forward for GET requests, but what if you’re sending a POST or DELETE request? Your test should also ensure that any desired business logic gets completed as expected.

API Testing is Integration Testing

Just like we use an integration tests to ensure that our app behaves as planned, we also require that our API responds as desired. These tests are based on HTTP requests to urls with calculated responses. For user interaction, Capybara is my testing tool of choice, but it is the wrong tool for testing APIs. Jonas Nicklas (creator of Capybara) wrote Capybara and Testing APIs to explain why you shouldn’t use it.

“Do not test APIs with Capybara. It wasn’t designed for it.” – Jonas Nicklas

Instead, use Rack::Test, rather than the Capybara internals.

Use RSpec Request Specs

Since we’ve established that we’ll be using Rack::Test to drive the tests, RSpec request specs make the most sense. There’s no need to get fancy and add extra weight to your testing tools for this.

Request specs provide a thin wrapper around Rails’ integration tests, and are designed to drive behavior through the full stack, including routing (provided by Rails) and without stubbing (that’s up to you).

To test requests and their responses, just add a new request spec. I’ll demonstrate testing a user sessions endpoint. My API returns a token on a successful login.

# spec/requests/api/v1/messages_spec.rb
describe "Messages API" do
  it 'sends a list of messages' do
    FactoryGirl.create_list(:message, 10)

    get '/api/v1/messages'

    expect(response).to be_success            # test for the 200 status-code
    json = JSON.parse(response.body)
    expect(json['messages'].length).to eq(10) # check to make sure the right amount of messages are returned

This works exceptionally well for get, post and delete requests. Just check for the status code you want, and that the response body is as you expected. That being said, with this setup we’ll be doing json = JSON.parse(response.body) a lot. This should be a helper method.

Add JSON Helper

To DRY things out for future tests, pull the json parsing logic into an RSpec helper. This is what I’ve done:

# spec/support/request_helpers.rb
module Requests
  module JsonHelpers
    def json
      @json ||= JSON.parse(response.body)

And then add the following line inside the config block of spec_helper.rb

RSpec.configure do |config|

  config.include Requests::JsonHelpers, type: :request


Now we can remove any of the JSON.parse(response.body) calls within our tests.

Let’s have another example spec, this time getting a single message.

# spec/requests/api/v1/messages_spec.rb
describe "Messages API" do
  it 'retrieves a specific message' do
    message = FactoryGirl.create(:message)    
    get "/api/v1/messages/#{message.id}"

    # test for the 200 status-code
    expect(response).to be_success

    # check that the message attributes are the same.
    expect(json['content']).to eq(message.content) 

    # ensure that private attributes aren't serialized
    expect(json['private_attr']).to eq(nil)

Okay – you’re done. Keep this in mind when you’re building out your api, and you’ll be golden. I promise. If you need some more info on how to set up your app as an API, I’d highly recommend this article: Building a Tested, Documented and Versioned JSON API Using Rails 4


  1. karlbright August 22, 2013 at 7:43 am #

    Your second example after the JSON helper is created has a misleading test description as “list of messages” :)

    • Matthew August 22, 2013 at 10:46 am #

      Thanks for pointing this out – I guess this is the downside of writing out code from memory. I’ve fixed it up.

  2. Nemo August 22, 2013 at 9:18 am #

    You’re missing a closing quote on this line in the final example:

    get “/api/v1/messages/#{message.id}

    Otherwise, great article! Thanks.

    • Matthew August 22, 2013 at 10:42 am #

      Thanks for the heads up! I’ve now fixed this.

  3. Tom Pesman August 26, 2013 at 1:22 am #

    I like the way you integrate the parsing of the JSON responses! But for an API you should be more strict on the response codes as be_success uses response.success? and is defined as a response within 200-299 and not only 200.


    I prefer:
    response.status.should eql(200)


    • Matthew August 28, 2013 at 2:24 pm #

      This is an excellent point.

      For POST requests, 201 created should be the response code and in other situations, explicitly stating the response status just makes sense.