Declarative HTTP API Testing with gabbi

While HTTP APIs are ubiquitous these days, testing and documenting such APIs remains somewhat awkward: Tests usually consist of procedural code that is specific to the respective language or even framework - which is neither very expressive nor easy to maintain.

gabbi promises relief by providing a language-agnostic, nicely readable and lightweight format for testing - and thus describing - HTTP APIs. Originally created as part of an effort to improve OpenStack’s APIs, it has since grown into a useful tool for anyone working with HTTP.

gabbi tests are simply YAML files that describe HTTP requests and responses. We’ll be using the wonderful httpbin for illustration purposes:

tests:

-name: supports unencrypted connections
  method: GET
  url: http://httpbin.org/
  status: 200

-name: supports SSL encryption
  method: GET
  url: https://httpbin.org/
  status: 200

method and status are optional, but it’s nice to be explicit. In fact, gabbi itself states that the name is “derived from ‘gabby’: excessively talkative” - i.e. gabbi prefers direct visibility and readability.

Of course we need to install gabbi in order to run these tests:

$ pip install gabbi

Using a virtual environment can help avoid sudo privileges or polluting our global packages directory.

Once that’s done, we can feed our YAML tests to gabbi:

$ gabbi-run < http.yaml
... E supports unencrypted connections
... E supports SSL encryption

ERROR: supports unencrypted connections
	[Errno 61] Connection refused
ERROR: supports SSL encryption
	[Errno 61] Connection refused
----------------------------------------------------------------------
Ran 2 tests in 0.015s

FAILED (errors=2)

Oops - I wasn’t connected to the internet. Of course we’d typically test a local development server instead, but that’s not important right now. Let’s try this again:

$ gabbi-run < http.yaml
... ✓ supports unencrypted connections
... ✓ supports SSL encryption

----------------------------------------------------------------------
Ran 2 tests in 0.681s

We don’t want to hard-code the target host, so let’s use paths instead of fully qualified URLs:

tests:

  -name: reports available HTTP methods
    method: OPTIONS
    url: /

    response_headers:
        allow: HEAD, OPTIONS, GET

  -name: front page returns HTML
    method: GET
    url: /

    status: 200
    response_headers:
        content-type: text/html; charset=utf-8

(Note that I added some vertical whitespace to separate request and response - that’s still valid YAML, of course.)

$ gabbi-run httpbin.org < http.yaml
... ✓ reports available HTTP methods
... ✓ front page returns HTML

----------------------------------------------------------------------
Ran 2 tests in 0.457s

That all seems pretty straightforward so far. Let’s ensure that the Allow header did in fact tell us the truth:

tests:

  -name: submitting data is not allowed
    method: POST
    url: /
    request_headers:
        content-type: text/plain
    data: lorem ipsum

    status: 405
$ gabbi-run httpbin.org < http.yaml
... ✓ submitting data is not allowed

As predicted, httpbin responded with 405 Method Not Allowed - clearly we’ll need to send our precious data to a different route:

tests:

  -name: form submission
    method: POST
    url: /post
    request_headers:
        content-type: application/x-www-form-urlencoded
    data: title=hello%20world&comment=lorem%20ipsum%21

    status: 200
$ gabbi-run httpbin.org < http.yaml
... ✓ form submission

Ah, there we go. Note that it’s our responsibility to encode the data there - that’s a little easier if we use JSON:

tests:

  -name: submitting JSON
    method: POST
    url: /post
    request_headers:
        content-type: application/json
    data:
        title: hello world
        comment: lorem ipsum dolor sit amet
$ gabbi-run httpbin.org < http.yaml
... ✓ submitting JSON

In fact, httpbin responds with JSON, returning in the response whatever data we submit in our request. We can use JSONPath to query that response data:

tests:

  -name: parsing JSON
    method: POST
    url: /post
    request_headers:
        accept: application/json
        content-type: application/json
    data:
        title: hello world
        comments: lorem ipsum dolor sit amet

    status: 200
    response_headers:
        content-type: application/json
    response_json_paths:
        $.json.title: hello world
        $.json.comments: lorem ipsum dolor sit amet
$ gabbi-run httpbin.org < http.yaml
... ✓ parsing JSON

It’s also possible to use data from the preceding response in a new request:

tests:

  -name: "consecutive redirects - step #1"
    method: GET
    url: /relative-redirect/2

    status: 302
    response_headers:
        location: /relative-redirect/1

  -name: "consecutive redirects - step #2"
    method: GET
    url: $LOCATION

    status: 302
    response_headers:
        location: /get
$ gabbi-run httpbin.org < http.yaml
... ✓ consecutive redirects - step #1
... ✓ consecutive redirects - step #2

Here we’re just using the Location header, but this technique also enables us to go full HATEOAS and test hypermedia APIs:

tests:

  -name: set up a hypermedia response
    method: PUT
    url: /put
    request_headers:
        accept: application/json
        content-type: application/json
    data:
        title: hello world
        links:
            next: http://httpbin.org/cookies/set?foo=bar

  -name: follow "next" link from preceding response
    method: GET
    url: $RESPONSE['$.json.links.next']

    status: 302
    response_headers:
        set-cookie: foo=bar; Path=/
$ gabbi-run httpbin.org < http.yaml
... ✓ set up a hypermedia response
... ✓ follow "next" link from preceding response

Obviously coercing httpbin into providing hypermedia responses is a bit of a hack, so we’ll stop here. Hopefully this is sufficient to get started - in a future post, we might expand on this with a more realistic example. We might also delve into gabbi’s extensibility, e.g. creating a custom response handler to query HTML responses with CSS selectors.

Feel free to join the IRC channel #gabbi on FreeNode or leave feedback via Twitter.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more