RSpec testing for a JSON API

Dana Hartweg
Jan 9, 2014 · 7 min read

RSpec is an alternative testing framework that replaces Minitest in Rails 4 (which is included by default). At first glance, there appears to be a lot of documentation floating around on how integrate RSpec into your Rails project. As you dig around you really only find varying opinions, strange (often unexplained) syntax, and you’re still left wondering how to get started. Worse, you might get started and realize you’re not writing tests in a way that makes sense to you. Then you end up reading more articles and opinions, and try to rewrite your tests… hoping they start to make sense.

Personally, I held off purchasing what amounts to the de facto book on RSpec: [Everyday Rails Testing with RSpec] ( “Everyday Rails Testing with RSpec book”) by Aaron Sumner. I should have started with this book, instead of getting to it after wading through everything else. The few dollars spent are well worth it. It’s not that you can’t find these code examples somewhere else, you certainly can… the big draw for me was the reasoning behind the tests, and why you should test for certain things. Organizing your test code is also covered.

API Testing versus Regular Testing

The biggest thing to note here is that API testing is quite different from testing a normal Rails application. There is no UI for the user to interact with (so there’s no need for Capybara or Cucumber), and everything has a response header. It’s also important to test for proper header responses, especially if the request doesn’t include a user token.

Your controller specs (especially in the #show and #index methods) will rely heavily on checking JSON responses for returned keys.

Test Helpers

I found it helpful to create two helpers for testing my API. These were the basis for most of the test requests and responses:

JSON Helper

Matthew Lehner has some great tips on his post about [api testing guidelines] ( “Rails API Testing Best Practices”), one of which includes a JSON helper to simplify and DRY up checking responses from the server. The code is as follows:

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

Then, you only have to configure RSpec to use the helper:

# spec/spec_helper.rb
config.include Requests::JsonHelpers, :type => :controller

Specifying the test type will prevent the helper from being loaded when you’re testing anything other than a controller. This will speed up your test times slightly.

To test for the returned JSON data in your controller specs, simply call the following:

expect(json).to have_key('test_key')

I wrote a small authorization helper that can be used in any controller spec (note: the name of the header can be anything… this is just the header I’m using for my tokens in my API). It simplifies the testing process so you don’t need to constantly add the access token to the request. It also allows you to easily clear any access token to test an unauthorized request. The helper (and configuration code) is as follows:

# spec/support/auth_helpers.rb
module AuthHelpers
def authWithUser (user)
request.headers['X-ACCESS-TOKEN'] = "#{user.find_api_key.access_token}"
def clearToken
request.headers['X-ACCESS-TOKEN'] = nil
# spec/spec_helper.rb
config.include AuthHelpers, :type => :controller

These methods can be used in the controller spec as follows (note: you would not want to run both methods before each test, as that would set and then clear the token… this is an example of the code you would use):

# setting authorization headers for the given user
before(:each) { authWithUser(user) }
# clearing any authorization headers
before(:each) { clearToken }


One of the biggest hangups I had was how to organize my specs, specifically my controller specs. As mentioned earlier, [Everyday Rails Testing with RSpec] ( “Everyday Rails Testing with RSpec book”) was very helpful with this. Aaron organized his tests in a way that made sense to me, and I only slightly adjusted them in order to test an API. Below are some sample model and controller specs (I’ve used [FactoryGirl] ( “FactoryGirl GitHub Project”) to generate my fixtures):

Model Spec

# model_spec.rb
let(:test_model) { build(:test_model) }
subject { build(:test_model) }

it { should be_valid }
it { should validate_presence_of :name }
it { should validate_uniqueness_of :name }

describe "#average_cost" do
context "with no purchases" do
it "is nil" do
expect(test_model.average_cost).to be_nil

context "with one or more purchases" do
let(:test_model_with_purchases) { create_list(:test_model_with_purchases, purchase_count: 2, cost: 100) }

it "calculates the average purchase price properly" do
expect(test_model_with_purchases.average_cost).to eq(100)

The model in question is specified both as a variable using the let() syntax, and the subject. This allows both one line tests and the more complex tests to run. I'm also using [Shoulda Matchers] ( "Shoulda Matchers GitHub Project") to test for rails associations, and valid attributes. This simplifies the process, and provides some extra flexibility.

Once I’ve tested the model attributes and any validations, I test any methods that may be associated with the model. I use a describe block to mention what method will be tested, and then proceed to test both common and edge cases for that method as a context. This allows me to perform any additional setup for a particular method within its own description block, and not have it crowd the main spec.

As the controller grows, more tests will be written for each context.

Controller Spec

Only samples for the #show and #index methods are included, to prevent things from getting too lengthy.

# controller_spec.rb
let(:user) { create(:user) }
let(:adminUser) { create(:admin) }

describe "with valid token", validToken: true do
before(:each) { authWithUser(user) }

describe "GET #show" do
let(:test_model) { create(:test_model) }
before(:each) { get :show, id: }

it "returns the information for one test_model" do
expect(json).to have_key('test_model')

it { should respond_with 200 }

describe "GET #index" do
let!(:test_models) { create_list(:test_model, 2) }

context "with id parameters" do
before(:each) { get :index, { :test_models => [ ] } }

it "returns a subset of test_models" do
expect(json['test_models'].count).to eq(1)

it { should respond_with 200 }

context "without id parameters" do
before(:each) { get :index }

it "returns all test_models" do
expect(json['test_models'].count).to eq(2)

it { should respond_with 200 }

describe "with admin token", adminToken: true do
before(:each) { authWithUser(adminUser) }

# there are no admin specific actions for the show and index methods
# the above code would authorize an admin user before each test

describe "without a valid token", noToken: true do
before(:each) { clearToken }
after(:each) { expect(response.status).to eq(401) }

it "GET #show is unauthorized" do
get :show, id: 0

it "GET #index is unauthorized" do
get :index

A few quick notes on what’s happening here. We’re defining the user types once at the top of the spec… let() is lazily evaluated, and won't be used until you need to authorize your requests. The tests are broken down into describe blocks based on what that user type can access. Some actions are restricted to admin users, and no one should be able to access information without a valid token. Each of these describe blocks contain a tag at the end of their definition. This allows us to run only the tests for a particular user type if we so choose.

Valid Token

We’re first authorizing a regular user before each request to the API. Then, we test each method based on what that regular user should be able to access. Each describe block has the setup needed for that method. You'll notice for the #show method, we're only creating one test_model, where as in the #index method, we're creating two. This prevents us from creating unnecessary data / database calls when they're not needed. This will help speed up our specs.

Admin Token

Any actions that only an admin should be able to perform would be located here. That could potentially be the #destroy or #patch methods. Since the admin is just a user, he can still perform all the actions a regular user can. There's no need to re-write the previous user specs.

No Token

Here we make sure our information is inaccessible if a valid token isn’t presented along with the request. Before each request we’re clearing any token that may have been set via before(:each) { clearToken }. We also know that all of these requests should return an unauthorized header, so it's safe to use the following line after(:each) { expect(response.status).to eq(401) } to check for that response after every test. Generally, you wouldn't want to run a check like this after every test. In this case, it DRY's up our spec quite a bit.


Hopefully this has been helpful! If you’d like some examples of more complex tests (including ones with model associations) I’d love to chat with you. This has been a huge learning experience for me, and I’d love to help others who may be in the same position.

I can’t claim this is the best, or 100% correct way to test your JSON API, but it’s what makes sense to me. As I continue to write more tests that cover differing scenarios, I’m sure I’ll end up modifying how I go about things. What’s the point if you’re not learning every step of the way?

Additional Resources

I found these resources helpful while figuring out how to write RSpec tests in a way that made sense to me. The RSpec Cheatsheet was especially useful for remembering proper syntax, as well as the multitude of conditions you can test for.

Originally published at on January 9, 2014.

    Dana Hartweg

    Written by

    Senior Front End Software Engineer, InVision Studio

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade