Rails API Testing Best Practices

on

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.

#!ruby
# 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
  end
end

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:

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

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

#!ruby
RSpec.configure do |config|

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

end

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.

#!ruby
# 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)
  end
end

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

  • http://twitter.com/karlbright karlbright

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

    • http://matthewlehner.net Matthew

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

  • Nemo

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

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

    Otherwise, great article! Thanks.

    • http://matthewlehner.net Matthew

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

  • http://tnux.net Tom Pesman

    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.

    https://github.com/rails/rails/blob/cd6301557005617583e3f9ca5fb56297adcce7cc/actionpack/lib/action_controller/test_process.rb#L173

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

    Cheers!

    • http://matthewlehner.net Matthew

      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.

  • Surya Arora

    is there any way by which we can check for something to not be in JSON response?

    • matthewlehner

      Hey Surya,

      Definitely – RSpec includes a number of built in matchers that should serve your needs. Have a look at the docs here: https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers

      For example, if you wanted to ensure that the JSON response didn’t have a specific root key, you could do something like this:


      expect(json.keys).not_to include('unwanted_key')

  • Ratnakarrao Nyros

    I am getting following error when I try to test api , its not finding response .

    undefined local variable or method `response’ for #

    Can you please help me how to fix it .

    • http://matthewlehner.net Matthew Lehner

      It sounds like you’re not calling `response` in the correct place. Make sure that it’s inside of an `it` block, and after making an HTTP request to the server.

      • Ratnakarrao Nyros

        I used it in correct place only , please look at my code

        specify “example description” do

        get “#{url}”

        expect(response.status).to eql 200

        end

        • http://matthewlehner.net Matthew Lehner

          If you’re using RSpec 3, you may have to specify that this is a request spec.


          describe "messages endpoint", type: :request do
          it "responds with a 200" do
          get "/messages"
          expect(response.status).to eq(200)
          end
          end

          I’ve pulled this straight from my test suite, removing the setup stuff, so I can confirm that it works.

        • Ratna Vanapalli

          yes you are correct thanks

        • Justin Gordon

          Or you can do this:

          RSpec.configure do |config|
          config.infer_spec_type_from_file_location!

        • http://matthewlehner.net Matthew Lehner

          Yeah, that’s true. Thanks for the note!

          Setting infer_spec_type_from_file_location! reintroduces the RSpec 2 behaviour. Personally, I prefer the newer, explicit default, but this will save typing and make an older test suite pass.

  • yazinsai

    Great resource, thanks Matthew!

  • Vladimir Tsymbal

    It’s very good article

  • Alex Friedman

    Great post Matthew. Just wanted to let you know, I actually co-authored a framework recently for just this purpose. Check it out:

    https://github.com/brooklynDev/airborne

    Would love some feedback if you have a minute.