Introduction

One of the things no one really likes to be doing is writing API documentation. But you know what is even worse than no documentation? Totally outdated documentation, where all the requests have changed and you spend a lot of time being misled by docs.

In this tutorial, we will use the DDD (documentation-driven development) approach to writing a kind-of-mocked Rails API application. We will set up extensive documentation testing using a combination of Rails API (our framework of choice), API Blueprint (as a language describing our API endpoints) and Dredd (a tool to test API endpoints against documentation).

What are we building?

We will build a very simple REST application providing two resources:

  • access_tokens (allows to log in and provides a bearer token used later)
  • messages

To keep things simple, creating a user has been left out.

Let’s start by creating a new Rails application in API mode:

$ rails new MessageAPI --api

And – since we want to follow DDD – it’s time to start writing documentation!

It’s time to start writing API docs for login

Our documentation will sit in the doc.apib file. Curious readers can read the whole tutorial, but that format is quite readable even without getting familiar with documentation.

FORMAT: 1A

# Messages API

# Authentication [/access_tokens]
## Login user [POST]

+ Request
    + Headers
    
            Content-Type: application/json; charset=utf-8
            
    + Body
    
            {
              "email": "[email protected]",
              "password": "password"
            }
            
+ Response 201
    + Headers
    
            Content-Type: application/json; charset=utf-8
            
    + Body
    
            {
              "access_token": "access_token"
            }

Ok, so we have the documentation for user login ready. Time for testing!

Let’s test our empty API using Dredd

It’s time to start using Dredd. As a preliminary, we need a node environment with npm (or Yarn). Let’s start by installing Dredd globally:

$ npm install -g dredd

We also need the dredd_hooks gem – this will allow us to use hooks for our documentation testing, providing us with a kind-of testing framework.

$ gem install dredd_hooks

Let’s generate Dredd configuration:

$ dredd init
? Location of the API description document doc.apib
? Command to start API backend server e.g. (bundle exec rails server) ./dredd_server.sh
? URL of tested API endpoint http://localhost:9865
? Programming language of hooks ruby
? Do you want to use Apiary test inspector? No
? Please enter Apiary API key or leave empty for anonymous reporter
? Dredd is best served with Continuous Integration. Create CircleCI config for Dredd? No

Configuration saved to dredd.yml
Install hooks handler and run Dredd test with:
  $ gem install dredd_hooks
  $ dredd

Since we want to customise enviromental variables (e.g., RAILS_ENV=test) for our Rails app, we need to create a custom command to run the server (in the file dredd_server.sh):

#!/bin/bash
# dredd_server.sh
export RAILS_ENV=test
export LOG_LEVEL=info
bundle exec rails server --port=9865

And we need to make it executable:

$ chmod +x dredd_server.sh

As in a  typical testing scenario, we will need some way to clean the database between requests, load some fixtures, etc. Fortunately, we have dredd_hooks – we can write custom pieces of Ruby code to be run in a specific moment. Due to the magic provided by Dredd (and by magic I mean carefully crafted pieces of engineering), this code will be ran before (or after) the provided transactions.

In the beginning, we will set hooks to clean the database after each test (same as in a typical testing scenario):

# dredd_hooks.rb
ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('config/environment', __dir__)
require 'dredd_hooks/methods'
require 'database_cleaner'

include DreddHooks::Methods

before_all do |_|
  DatabaseCleaner.strategy = :truncation
  DatabaseCleaner.clean_with(:truncation)
end

after_each do |_|
  DatabaseCleaner.clean
end

And add it to the dredd.yml file:

hookfiles: "dredd_hooks.rb"

Of course we also need to install database_cleaner, by adding it to the Gemfile (in the test group):

gem 'database_cleaner', group: :test

We can also check the syntax of our APIB file and display all transactions (i.e., the requests to be made to the backend) by running commands:

$ cd ..
$ dredd MessageAPI/doc.apib http://localhost:3000 --names
info: Beginning Dredd testing...
info: Authentication > Login user
skip: POST (201) /access_tokens
complete: 0 passing, 0 failing, 0 errors, 1 skipped, 1 total
complete: Tests took 7ms

Time to run the server!

Running our first test

Running the dredd command will result in an error:

$ dredd
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Starting backend server process with command: ./dredd_server.sh
info: Waiting 3 seconds for backend server process to start
info: Beginning Dredd testing...
info: Found Hookfiles: 0=/Users/esse/work/rebased/MessageAPI/dredd_hooks.rb
info: Spawning 'ruby' hooks handler process.
warn: Error connecting to the hooks handler process. Is the handler running? Retrying.
info: Successfully connected to hooks handler. Waiting 0.1s to start testing.
2018-06-25 20:41:05 +0200: Rack app error handling request { POST /access_tokens }
#<ActionController::RoutingError: No route matches [POST] "/access_tokens">complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total

That’s cool – we’re sending a POST request to a route which doesn’t exist yet. So let’s write a minimal stub that will satisfy our requirement.

Documentation ready, time to write some code

We will need an AccessTokensController:

# app/controller/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  def create
    render json: { access_token: 'ABC' }, status: :created
  end
end

And route to it:

# config/routes.rb
Rails.application.routes.draw do
  resources :access_tokens, only: :create  
end

Let’s run Dredd now:

$ dredd
…
pass: POST (201) /access_tokens duration: 39ms
info: Hooks handler stdout: /Users/esse/work/rebased/MessageAPI/dredd_hooks.rb
Starting Ruby Dredd Hooks Worker...
Dredd connected to Ruby Dredd hooks worker

complete: 1 passing, 0 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 1266ms

(If the test still fails try to manually kill the puma process; sometimes it’s not killed properly.)

You may have noticed that we returned a different access token than the one provided in the API docs – actually, Dredd generates a JSON schema out of the example responses (you may also provide your own JSON schema if you fancy that kind of thing) and checks compliance. This way you don’t have to worry about resetting sequences for primary keys, etc.

Documenting message

For the following actions we will require the user to provide a valid bearer token – otherwise we return the 401 (unauthorized) status. We will use standard Rails REST actions (only index and show).

# Message [/messages]

## Get all messages [GET]

+ Response 401

+ Request
    + Headers
    
            Content-Type: application/json; charset=utf-8
            Authorization: Bearer ABC123
            
+ Response 200
    + Headers
    
            Content-Type: application/json; charset=utf-8
            
    + Body
    
            [
              {
                "id": 1,
                "content": "Message 1"
              }
            ]
            
# Message [/messages/{id}]

## Show single message [GET]

+ Parameters
    + id: `1` (number, required) - Id of a message.
    
+ Response 401

+ Request
    + Headers
    
            Content-Type: application/json;charset=utf-8
            Authorization: Bearer ABC123
            
+ Response 200
    + Headers
    
            Content-Type: application/json; charset=utf-8
            
    + Body
    
            {
              "id": 1,
              "content": "Message 1"
            }

And of course, right now running dredd will result in an error:

$ dredd
…
pass: POST (201) /access_tokens duration: 38ms
fail: GET (401) /messages duration: 12ms
fail: GET (200) /messages duration: 17ms
fail: GET (401) /messages/1 duration: 11ms
fail: GET (200) /messages/1 duration: 13ms
…

It’s time to write our authentication stub and real messages model (to show how to make hooks work in transactions).

First, let’s write a controller with actions and access control – and wire it up in the routes:

# app/controller/messages_controller.rb
class MessagesController < ApplicationController
  before_action :check_access_token
  
  def index
  end
  
  def show
  end
  
  private
  
  def check_access_token
    head :unauthorized unless request.headers["AUTHORIZATION"]
  end
end
# config/routes.rb
resources :messages, only: [:index, :show]

And now it’s time to test our documentation:

$ dredd
…
pass: POST (201) /access_tokens duration: 101ms
pass: GET (401) /messages duration: 12ms
fail: GET (200) /messages duration: 20ms
pass: GET (401) /messages/1 duration: 11ms
fail: GET (200) /messages/1 duration: 14ms

Great! Now we only need to provide index and show with objects rendered from the database.

$ rails g model Message content:text
$ rails db:migrate
$ rails db:test:prepare

We will use the simplest serialization type, providing a method in the model:

# app/models/message.rb
class Message < ApplicationRecord
  def as_json(*)
    {
      id: id,
      content: content
    }
  end
end

Also, let’s provide scaffold-like code in controller:

# app/controller/messages_controller.rb
# …

def index
  render json: Message.all
end

def show
  render json: Message.find(params[:id])
end

It’s testing time!

$ dredd
…
pass: POST (201) /access_tokens duration: 104ms
pass: GET (401) /messages duration: 15ms
pass: GET (200) /messages duration: 40ms
pass: GET (401) /messages/1 duration: 13ms
2018-06-25 21:17:30 +0200: Rack app error handling request { GET /messages/1 }
#<ActiveRecord::RecordNotFound: Couldn't find Message with 'id'=1>
fail: GET (200) /messages/1 duration: 23ms

Oh… We need to create an object with id of 1 – otherwise Rails will raise an ActiveRecord error. Here come Dredd hooks (the index action passes because an empty array is compliant with the generated JSON schema).

First, let’s check our transactions names (again):

$ dredd MessageAPI/doc.apib http://localhost:3000 --names
info: Beginning Dredd testing...
info: Authentication > Login user
skip: POST (201) /access_tokens
info: Message > Get all messages > Example 1
skip: GET (401) /messages
info: Message > Get all messages > Example 2
skip: GET (200) /messages
info: Message > Show single message > Example 1
skip: GET (401) /messages/1
info: Message > Show single message > Example 2
skip: GET (200) /messages/1
complete: 0 passing, 0 failing, 0 errors, 5 skipped, 5 total
complete: Tests took 8ms

Great – we need to write a before_hook for the Message > Show single message > Example 2 transaction, so let’s write it:

# dredd_hooks.rb
# …
before 'Message > Show single message > Example 2' do |_|
  Message.create!(id: 1, content: "Some message")
end

And now, the time of truth:

$ dredd
…
pass: POST (201) /access_tokens duration: 43ms
pass: GET (401) /messages duration: 14ms
pass: GET (200) /messages duration: 22ms
pass: GET (401) /messages/1 duration: 14ms
pass: GET (200) /messages/1 duration: 25ms
complete: 5 passing, 0 failing, 0 errors, 0 skipped, 5 total
complete: Tests took 1412ms

Yay! The whole process can be of course integrated into the CI flow – to ensure that documentation is always up-to-date before merging a PR.

We can also generate a pretty HTML version of the docs by using the aglio tool:

$ npm install -g aglio
$ aglio -i doc.apib -o doc.html

This is how it looks.

The complete example can be found in a GitHub repository.

Conclusion

We introduced DDD – documentation-driven development, that together with an API description language (APIB) and a testing tool (Dredd) provides us with an easy framework to write always up-to-date documentation. Hey, it also improves our tests coverage and generates pretty HTML. Nice!