RESTUnit

RESTUnit is a set of Java classes that can be used to test REST services. The data structure classes shall be serializable to and from a simple format, making the simplest way to create tests a matter of creating text files.

Main features:

Usage

Currently, RESTUnit can be used as a TestNG test, with tests created programmatically.

You simply extend TestNGRestUnit, set up the RestUnit instance and implement getTests() to return your programmatically created tests. Suppose you wanted to test that your REST service returns a particular XML document when you request /rest/pictures and that it sends a 401 when you try to PUT there:

public class PicturesTests extends TestNGRestUnit
{
    public PicturesTests()
    {
        super();
        setRestUnit(new RestUnit());
        getRestUnit().getExecutor().setHttp(new JavaHttp());
        getRestUnit().getExecutor().setBaseURL("http://your.domain.com/rest");
    }

    public Set<RestTest> getTests()
    {
        Set<RestTest> tests = new HashSet<RestTest>();

        tests.add(createGetTest());
        tests.add(createPutTest());

        return tests.
    }

    private RestTest createGetTest()
    {
        RestTest test = new RestTest();
        test.setURL("/pictures");
        GetCall get = new GetCall();
        get.setMethod("GET");
        get.setName("GET to /pictures, which is allowed");
        BodyResponse response = new BodyResponse();
        response.setContentType(new ContentType("text/xml","UTF-8"));
        response.setStatusCode(200);
        response.getRequiredHeaders().put("Last-Modified");
        response.getRequiredHeaders().put("ETag");
        String data = "<pictures>" +
            "<picture><name>foo</name><location>/pictures/foo.jpg</location></picture>" +
            "<picture><name>bar</name><location>/pictures/bar.jpg</location></picture>" +
            "<picture><name>The Great Foo</name><location>/pictures/great_foo.jpg</location></picture>" +
            "</pictures>";
        response.setBody(data.getBytes("UTF-8"));
        get.setResponse(response);
        test.addTest(get);
        return test;
    }

    private RestTest createPutTest()
    {
        RestTest test = new RestTest();
        test.setURL("/pictures");
        RestCall put = new RestCall();
        put.setMethod("PUT");
        put.setName("PUT to /pictures, which isn't allowed");
        RestCallResponse response = new RestCallResponse();
        response.setStatusCode(401);
        put.setResponse(response);
        test.addTest(put);
        return test;
    }
}

This will run two seprate tests.

Granted, this seems actually slightly more work than just using an HTTP client. The intention of RESTUnit is that you should not normally have to create tests programmatically, nor should you have to configure your test environment programmatically. Further, RESTUnit will provide a means of deriving tests. Taking the above example, and assuming our service supports the standard features of HTTP and REST, we could derive several tests:

Details/Discussion

Test Form

A REST test is much simpler than an arbitrary unit test. It is a matter of HTTP requests and responses.

A test of a REST service endpoint (which would be a resource, identified by a URL) is a series of REST calls. This could be simply one call (a GET to a resource that should return known data), or a more complex interaction of PUTing a new resource to the server, GETing that resource to see if it was received and DELETEing it to leave the remote data store in a known state.

REST Call

We'll use the term "Call" to be one HTTP request and response cycle.

The request can be as simple as:

The response is equally simple:

We could represent such a call in YAML as so:

url: /accounts/Initech/users
method: GET
parameters:
    - name: *bob*
    - sort: { ascending: true, by: last name}
headers:
    If-None-Match: 71783837e5a4c543bce456
response:
    status: 200
    body: <users><user id="34"><name>Bob 1</name></user><users id="556"><name>Bob 2</name></user></users>
    headers:
        Content-Type: { value: text/xml }
        Last-Modified: { required: true }

Rest Test

A full-on test would be a series of calls. This could easily be described using the format above. Further, since a series of calls is made against the same URL, we can promote the URL up to the test level. Calls could override the URL as needed (as for a POST that would create a new URL):

url: /accounts/Initech/users/bolton
- POST:
    - url: /accounts/Initech/users
    - stuff to put data
- GET: # uses the parent URL
    - stuff for the get test
- DELETE
- GET
    response:
        status: 404

So far, this just looks like a simplistic way to test any HTTP endpoint. However, we can use this tests to derive new ones, based upon the conventions of REST (or conventions your REST service provides), such as:

Such derived tests would be virtually identical to their base counterparts, so why copy them? Consider the URL /accounts/Initech/users/bob/profile.xml and supposed your REST service only sends ETags for JPEGs. You could test this via:

url: /accounts/Initech/users/bob/profile.xml
- GET:
    respondsToIfNonMatch: false
    response:
        status: 200
        content: <profile id="234"><name>Mike Bolton</name></profile>
        contentType: text/xml

We could then derive this test:

url: /accounts/Initech/users/bob/profile.xml
- GET:
    respondsToIfNonMatch: false
    response:
        status: 200
        content: <profile id="234"><name>Mike Bolton</name></profile>
        contentType: text/xml
        headers:
            Last-Modified {required: true}
- GET:
    respondsToIfNonMatch: false
    respondsToIfModifiedSince: false
    headers:
        If-Modified-Since: $Last-Modified$ # indicates to use the last tests response header
    response:
        status: 304
        content: <profile id="234"><name>Mike Bolton</name></profile>
        contentType: text/xml
- HEAD:
    response:
        status: 200
        # body omitted means no body should be returned 
- HEAD:
    headers:
        If-Modified-Since: 2008-01-01
    response:
        status: 304

Comparing Results

In a lot of cases, simple byte-for-byte comparisons of results could work. Out of the box, we can provide: * Ensure equality of status codes * Check that certain headers are included (regardless of value) * Check that certain headers are not included * Check that certain headers are included and have a specific value * byte-for-byte comparison of body received and body expected.

Further, we can provide a means of customizing the comparison. For example, a user may be expecting XML that has temporal data in it. If this can be ignored for the sake of the test, this should be easy to accomplish.

Output Format

JUnit's XML results file seems to be ubiquitous. RESTUnit must output that so that integration with tools like Bamboo are possible. Support for TestNG's output format may also be desirable, though this could be most easily achieved by allow RESTUnit to run as a TestNG test. Making up a new output format is probably NOT desirable.

Links