Posts Tagged ‘yaml’

Test REST Services

Friday, September 12th, 2008

In my reply to a post on Tim Bray’s blog about using RSpec for testing REST services, I briefly described a project I’m working on, based on the work I’ve been doing at Gliffy, which is a testing framework for REST services called, unsurprisingly, RestUNIT.

For Gliffy’s REST-based integration API, I needed a way to test it, and hand-coding test cases using HTTPClient was just not going to cut it. Further, requests to Gliffy’s API require signing (similar to how Flickr does it), and our API was going to support multiple ways of specifying the representation type as well as tunneling over POST.

So, it occured to me that there was a lazier way of doing this testing. All I really needed to specify was the relative URL, parameters, headers, method, and expected response. Someone else could do the signing and re-run the tests with the various options (such as specifying the MIME Type via the Accept: header, and then again via a file extension in the URL).

I ended up creating a bunch of text files with this information. I then used a Ruby script to generate two things: an XML file that could be deserialized into a java object useful for testing, and a PHP script to test our PHP client API.

The Ruby script would also do things like calculate the signature (the test text files contained the api and secret keys a Gliffy user would have to use the API) and generate some derivative tests (e.g. one using a DELETE, and another tunneling that over POST). The testing engine could generate some additional derivative tests (e.g. GET requests should respond to conditional gets if the server sent back an ETag or Last-Modified header). All this then runs as a TestNG test.

The whole thing works well, but is pretty hackish. So, RestUNIT was created as a fresh codebase to create a more stable and useful testing engine. My hope is to specify tests as YAML or some other human-readable markup, instead of XML (which is essentially binary for any real-sized data) and to allow for more sophisticated means of comparing results, deriving tests, and running outside a container (all the Gliffy tests require a specific data set and run in-container).

The test specification format should then be usable to generate tests in any other language (like I did with PHP). I’m working on this slowly in my spare time and trying to keep the code clean and the architecture extensible, but not overly complex.

Schema for REST services

Thursday, September 11th, 2008

I’m currently working the integration API for Gliffy, which is a REST-based service. The API is fairly stable and we’re readying a few ancillary things for release. One of those is the documentation for the API. I found it quite difficult to completely describe the REST services and ultimately ended up creating something that lists out “objects” and “methods”, even though the API is not really object-based. For example, the object “Diagram” has a “method” called “list”; to “call” it, you do an HTTP GET to accounts/your account name/diagrams.

The original spec I created to work against (and thus, our initial draft of API documentation) was basically a list of URLs and the HTTP methods they responded to. Not very easy to navigate or understand on a first sitting. Some sort of schema to describe the REST API would have been really helpful (along the lines of an XML Schema). Such a schema could facilitate documentation, testing, code generation.

As an example, consider some features of the Gliffy API: you can list the users in an account, list the diagrams in an account and reference an individual diagram via id. Here’s a YAML-esque description of these services:

accounts:
  kind: literal
  desc: "Reference to all accounts"
  POST:
    desc: "Creates a new account"
    parameters:
        - account_name
            required: true
            desc: "Name of the account you want to create"
        - admin_email
            required: true
            desc: "Email address of an administrator for the new account"
  children:
     account_name:
       kind: variable
       desc: "The name of your account"
       GET:
         desc: "Returns meta-data about the account"
         parameters:
            - show_users
              required: false
              desc: "If true, users are included, if false, they are not"
       children:
         diagrams:
           kind: literal
           desc: "All diagrams in the account"
           POST:
             desc: "Creates a new diagram"
             parameters:
               - diagram_name
                 required: true
                 desc: "Desired name for this diagram"
               - template_id
                 required: false
                 type: numeric
                 dsec: "If present, the id of the diagram to copy, instead of using the blank one"
           GET:
             desc: "Gets a list of all diagrams in this account"
           children:
             id:
               kind: variable
               type: numeric
               desc "The id of a particular diagram"
               GET:
                 desc: "Gets the diagram; the requested encoding type will determine the form"
                 parameters:
                   - version:
                     desc: "The version to get, 1 is the original version.  If omitted, current version is retrieved"
                     required: false
                     type: numeric
                   - size:
                     desc: "For rastered formats, determins the size
                     type: enumeration
                       - L
                       - M
                       - S
               DELETE:
                 desc: "Deletes this image"
          users:
            kind: literal
            desc: "All users in the account"
            GET:
              desc: "gets a list of all users in the account"

Since “accounts” is the only top-level element, we are saying that every request to this service must start with accounts/. It has one child, which is a variable value for the account name. It is untyped, so any potential string is allowed. That element has two possible children: diagrams and users. diagrams indicates that it responds to the HTTP methods POST and GET. A POST requires the parameter diagram_name, while the parameter version is optional.

A standard format like this could easily be used to generate documentation, expectations, test cases, and even stub code. This format could even be delivered by an OPTIONS call to a resource. I realize there is not much standardization around how to design and implement a REST service, but something like this could at least be a stake in the ground and support a specific method.