Mock APIs with API Blueprint, Dredd, api-mocks, and rails

Here's a story we're all too familiar with: you're developing an API and webapp at the same time. API first is all the rage these days. At some point frontend developers will need to connect to a backend API. They can mock out API behavior for a little while, but may wind up in trouble if the final API is very different than what was expected. You don't want to rush API development, but you need to share something with the people developing the frontend...sooner rather than later.

Now this may sound obvious, but the thing about coding is this: you don't just start writing code. Instead, you spend a good chunk of time figuring out what to build. The better idea you have of what needs to be built, the faster you'll be able to build it once you get started. You're also less likely to build features that aren't needed or introduce unnecessary code (less is more!). Just like the frontend, the design phase is equally important for the API.

Rather than starting to code the API, wouldn't it be nice if you could:

Thankfully there are several such API modeling languages out there that make it relatively straightforward to accomplish the goals above. The main competitors are:

Looking for some comparisons? Check out the following articles:

The general consensus seems to be that there's no clear winner in the API modeling language game. PLUS--there's tools to convert between modeling languages, so migration is always a potential option should one of the modeling langauges become deprecated down the road.

For my project, I chose API Blueprint for the following reasons:

Ok, so how do I get started with all of this?

Start by installing dependencies

Please note that I'm working on a Mac and using Homebrew so my notes are for that kind of setup.

node (using v0.10.40)

dredd and api-mock are node tools so you'll need node.

brew install nodejs

dredd (v1.0.1) / api-mock (v0.2.2)

Use npm to install dredd and api-mock:

npm install -g dredd api-mock

ruby2.2 / rails4.2

This is documented here. If you're on Yosemite and experiencing any OpenSSL issues, take a look at this article. Once ruby is installed you can install rails and bundler like this:

gem install --no-ri --no-rdoc rails bundler

Great, now we can get started.

Time to create the dummy API

We want our API to have a single endpoint located at /dummy. It will respond to a GET request with the following plaintext: hello world. If we're using curl, it would behave like this:

$ curl -v -X GET localhost:3000/dummy
*   Trying ::1...
* connect to ::1 port 3000 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /message HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/plain
< Content-Length: 17
< Date: Mon, 23 Nov 2015 06:00:07 GMT
< Connection: keep-alive
<
hello world
* Connection #0 to host localhost left intact

API Blueprint

The associated specification (a.k.a "spec") file describing this API would look like this:

dummyapi.apib

# My API
## GET /dummy
+ Response 200 (text/plain)

  hello world

Mock API Server

To run this mock API, run this:

$ api-mock ./dummyapi.apib --port 3000                                           
info:    Enabled Cross-Origin-Resource-Sharing (CORS)
info:     Allow-Origin: *
info:     Allow-Methods: GET, PUT, POST, PATCH, DELETE, TRACE, OPTIONS
info:     Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Referer, Prefer
info:    Listening on port 3000

You now have a dummy mocked API (on http://localhost:3000). Sweet!

$ curl -v -X GET localhost:3000/dummy
*   Trying ::1...
* connect to ::1 port 3000 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /message HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/plain
< Content-Length: 17
< Date: Mon, 23 Nov 2015 06:00:07 GMT
< Connection: keep-alive
<
hello world
* Connection #0 to host localhost left intact

Implementation Time

All is fine and dandy, but what do you do when you want to start implementing this api in some language like ruby, python, go, etc? That's where dredd comes in. The tagline for dredd is:

> Dredd is a language agnostic command-line tool for testing API documentation written in the API Blueprint format against its backend implementation.

This means that:

Configure dredd

dredd has an interactive configuration mode so you can get started quickly. For our example, run dred init to get started:

$ dredd init
? Location of the API blueprint: apiary.apib
? Command to start API backend server e.g. (bundle exec rails server) bundle exec rails s
? URL of tested API endpoint: http://localhost:3000
? Programming language of hooks: ruby
? Do you want to use Apiary test inspector? No
? Found CircleCI configuration, do you want to add Dredd to the build? No

Configuration saved to dredd.yml

Install hooks handler and run Dredd test with:

  $ gem install dredd_hooks
  $ dredd

This results in a dredd.yml file that looks like this:

dry-run: null
hookfiles: ./hooks.rb
language: ruby
sandbox: false
server: bundle exec rails s
server-wait: 3
init: false
custom: {}
names: false
only: []
reporter: []
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
blueprint: apiary.apib
endpoint: 'http://localhost:3000'

NOTE: Ignore the hookfiles part right now. We'll get to that soon.

Implement the dummy API

Before creating the implementation, lets see what happens if we just try running this by itself:

$ dredd
Configuration dredd.yml found, ignoring other arguments.
Starting server with command: bundle exec rails s
Waiting 3 seconds for server command to start...
Could not locate Gemfile or .bundle/ directory
info: Beginning Dredd testing...
error: GET /dummt duration: 4ms
error: Error connecting to server under test!
info: Displaying failed tests...
fail: GET /dummy duration: 4ms
fail: Error connecting to server under test!
complete: 0 passing, 0 failing, 1 errors, 0 skipped, 1 total
complete: Tests took 9ms

Dredd attempted to start up our api...and it didn't exist. Can't test something that doesn't exist! I'll quickly setup a quick rails app.

$  rails new dummyapi
      create
      create  README.rdoc
      ...
Bundle complete! 12 Gemfile dependencies, 53 gems now installed.

And run dredd again:

$ dredd
Configuration dredd.yml found, ignoring other arguments.
Starting server with command: bundle exec rails s
Waiting 3 seconds for server command to start...
[2015-11-22 23:01:20] INFO  WEBrick 1.3.1
[2015-11-22 23:01:20] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2015-11-22 23:01:20] INFO  WEBrick::HTTPServer#start: pid=52324 port=3000
info: Beginning Dredd testing...
fail: GET /dummy duration: 447ms
info: Displaying failed tests...
fail: GET /dummy duration: 447ms
fail: headers: Header 'content-type' has value 'text/html; charset=utf-8' instead of 'text/plain; charset=utf-8'
body: Real and expected data does not match.
statusCode: Status code is not '200'

request:
body:

headers:
    User-Agent: Dredd/1.0.1 (Darwin 15.0.0; x64)

uri: /dummy
method: GET


expected:
headers:
    Content-Type: text/plain; charset=utf-8

body:
hello world

statusCode: 200


actual:
statusCode: 404
headers:
    content-type: text/html; charset=utf-8
    content-length: 37322
    x-web-console-session-id: 5266580f4d873b0f4c7f9fd7a0bd0e6d
    x-request-id: 10e10910-d808-4b8c-a0e7-11bbd8c851db
    x-runtime: 0.283871
    server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
    date: Mon, 23 Nov 2015 07:01:22 GMT
    connection: Keep-Alive

body:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Action Controller: Exception caught</title>
...
complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 451ms

We now see 1 failing test (/dummy), with the following errors:

I haven't set any endpoints up yet, so these errors are letting me know what's missing.

routes.rb

Rails.application.routes.draw do
  get 'dummy', to: 'api#dummy'
  ...
end

app/controllers/api_controller.rb

class ApiController < ApplicationController

  def dummy
    render plain: "hello world"
  end

end

Now to test again...

$ dredd
Configuration dredd.yml found, ignoring other arguments.
Starting server with command: bundle exec rails s
Waiting 3 seconds for server command to start...
[2015-11-22 23:08:44] INFO  WEBrick 1.3.1
[2015-11-22 23:08:44] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2015-11-22 23:08:44] INFO  WEBrick::HTTPServer#start: pid=52409 port=3000
info: Beginning Dredd testing...
fail: GET /dummy duration: 155ms
info: Displaying failed tests...
fail: GET /dummy duration: 155ms
fail: body: Real and expected data does not match.

request:
body:

headers:
    User-Agent: Dredd/1.0.1 (Darwin 15.0.0; x64)

uri: /dummy
method: GET


expected:
headers:
    Content-Type: text/plain; charset=utf-8

body:
hello world

statusCode: 200


actual:
statusCode: 200
headers:
    x-frame-options: SAMEORIGIN
    x-xss-protection: 1; mode=block
    x-content-type-options: nosniff
    content-type: text/plain; charset=utf-8
    etag: W/"bd13b94ec091c54f6f01d47ce47a54a5"
    cache-control: max-age=0, private, must-revalidate
    x-request-id: bc936f24-2dcb-4e96-8c03-3673d8edaf01
    x-runtime: 0.114245
    server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
    date: Mon, 23 Nov 2015 07:08:46 GMT
    content-length: 16
    connection: Keep-Alive

body:
hello world

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

What's going on here? Well, it turns out there there's some extra whitespace in a text/plain response. This is documented here. This is where the hook system comes into play.

Dredd hooks

Dredd hooks are documented here. Common use cases for hooks include (taken from the dredd documentation):

In our case we can use the hooks to clean up the newline character in the expected result. For ruby/rails, this means that we'll need to install the dredd_hooks gem:

gem install --no-ri --no-rdoc dredd_hooks

Then we'll create a hooks.rb file that looks like the following:

hooks.rb

require 'dredd_hooks'
include DreddHooks::Methods

before_each do |transaction|
  if transaction['expected']['headers']['Content-Type'].match(/^text\/plain/)
    transaction['expected']['body'] = transaction['expected']['body'].gsub(/^\s+|\s+$/, "")
  end
end

Now to run dredd again (important: hookfiles must be set in dredd.yml to ./hooks.rb):

$ dredd
Configuration dredd.yml found, ignoring other arguments.
Starting server with command: bundle exec rails s
Waiting 3 seconds for server command to start...
[2015-11-22 23:17:31] INFO  WEBrick 1.3.1
[2015-11-22 23:17:31] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2015-11-22 23:17:31] INFO  WEBrick::HTTPServer#start: pid=52619 port=3000
info: Beginning Dredd testing...
Native thread-sleep not available.
This will result in much slower performance, but it will still work.
You should re-install spawn-sync or upgrade to the lastest version of node if possible.
Check /usr/local/lib/node_modules/dredd/node_modules/spawn-sync/error.log for more details
Spawning `ruby` hooks handler
Hook handler stdout: ./hooks.rb
Starting Ruby Dredd Hooks Worker

Hook handler stderr: Dredd connected to Ruby Dredd hooks worker

pass: GET /message duration: NaNms
complete: 1 passing, 0 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 2245ms

Success! You have now accomplished the following: