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
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!