Integration testing for Rails APIs part 1: Structuring specs

Writing an API with Rails? Learn how to avoid making a mess when testing. Structure your integration specs in a logical manner to make maintaining them a lot easier.

Lately I’ve been working on a few Rails-backed APIs. Most of them are based on the excellent rails-api gem that puts together a lighter version of Rails specifically with APIs in mind.

To build a stable API, I need tests to make sure that any changes won’t suddenly break compatibility. There’s no way I’m building an API without tests: they help me refactor with confidence during the day and sleep soundly at night!

A three-part series

This was supposed to be a single post about how I do integration testing, but apparently I had way too much to say. Let’s call this part one then, shall we?

This series is divided into three parts:

  1. Structuring specs
  2. Writing less code
  3. Automating tedious things

I’m writing on an intermediate level here; I assume that you have a pretty good grasp on BDD jargon and you’ve done testing with Rails before.

First let’s lay the groundwork for the next two parts. In this part I’ll show you how to organise and write your specs in a more manageable way.

Let’s get started!

It’s easy to make a mess

The thing that makes effective integration testing difficult is that it’s easy to make a mess with duplicated code and lengthy examples.

Working with an API just makes this issue worse: the output is well defined, so it’s tempting to just throw lots of expectations together (maybe copy some from another spec) and call it a day.

You’ll start to notice this problem when your specs are hundreds of lines long and you have trouble navigating through the sea of duplicated code.

One endpoint, one method, one file

Shorter files are easier to manage. Describing an API endpoint with all of its calls in a single file is usually too lengthy, but one API call per file has worked well for me.

Let’s imagine that you have a /users endpoint with these calls:

GET      /users       UsersController#index
POST     /users       UsersController#create
GET      /users/:id   UsersController#show
PATCH    /users/:id   UsersController#update
DELETE   /users/:id   UsersController#destroy
Simple RESTful routes that map to a controller.

I usually name my specs according to their controller actions so that the specs are named uniquely within their subfolder.

The endpoint above would have these specs:

├── create_spec.rb
├── destroy_spec.rb
├── index_spec.rb
├── show_spec.rb
└── update_spec.rb
Listing for the directory that contains specs for the /users endpoint.

And I’d describe the HTTP method and path with parameters, if any:

RSpec.describe 'GET /users/:id' do
  # ...
This would be show_spec.rb.

Using let and context to structure specs logically

Remember when we talked about making a mess? Luckily RSpec has a bunch of features that help you reduce duplicated code. let and context are my favourite ones since they work so well together.

The let method defines a helper method that’s lazily evaluated when used for the first time in an example. It’s not shared across examples, which makes it ideal for defining “variables” at the top of your tests.

The context method defines a nested group of examples. Like you’d expect, it’s used for adding context to each of its sub-examples, e.g. “when the user exists, these examples should pass.”

When used together, these two become your BDD besties. You can use let to establish a described context:

context 'with a regular user' do
  let(:user) { User.create(admin: false) }
  it 'denies access'

context 'with an admin user' do
  let(:user) { User.create(admin: true) }
  it 'grants access'
Using helper methods to establish a given context.

But it gets even better! Since helpers defined with let are not shared across examples, you can redefine them within an example group.

The logical next step is to define defaults for the common case at the top of the spec and then override them based on the context. You can even reference other helpers:

let(:name)   { 'John Doe' }
let(:admin?) { false }
let(:user)   { User.create(name: name, admin: admin?) }

context 'with a regular user' do
  it 'denies access'

context 'with a regular user named Leo' do
  let(:name) { 'Leo Nikkilä' }
  it 'releases the hounds'

context 'with an admin user' do
  let(:admin?) { true }
  it 'grants access'
Redefining defaults in later contexts. Note that the first context doesn’t override any helpers since it’s the default case.

With a structure like this, you can easily pinpoint what’s being changed within a given context. There’s no need to duplicate any of the default values.

Note that the default case (the one not overriding any of the defaults) is described first. That’s a good habit to get into for readability.

To reiterate: use let to set up the default (or positive) case at the top of the file. Then change one thing in each subsequent context by overriding the defaults.

before is where the request happens

With rspec-rails you get a bunch of helpers like get and post that help you make those requests. After calling one of them, you have response at your disposal.

Don’t stick the request right inside the example. We’re describing a single API call per file, so we’re basically testing the same request with different parameters, right?

When using let and context, you never have to repeat the request as long as you declare the variables (if any) and stick the request in a before block.

You can then manipulate just the bits you need:

RSpec.describe 'GET /users/:id' do
  let(:user) { User.create(name: 'John Doe') }
  let(:id)   { }

  before { get "/users/#{id}" }

  context 'when the user exists' do
    # ...

  context 'when a user is not found with the ID' do
    let(:id) { -1 }

    it 'returns HTTP 404' do
      expect(response.status).to eq 404
Manipulating a request defined in a before block in a later context.

What’s next

These are just a few techniques that help you organise your specs. Check out for more RSpec best practices.

My next post in this series will be about things that help you write less testing code. I’ll be sending out a newsletter when it’s ready so make sure to subscribe below if you’re interested.

As always, leave a comment or tweet at me if you have any questions, comments or criticism! See you again in the next post.

Related posts