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)

(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