Testing a HATEOAS api with RestAssured

In this post I describe how a REST API that follows HATEOAS can be easily tested in Java using RestAssured framework.

Before I come to that, first a short explanation what that strange acronym means.

A RESTful api is not complete without HATEOAS. As Joe Fielding tells in his blog1What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint?

HATEOAS

What sounds like an organisation of angry citizens is the acronym of that constraint: HATEOAS. It stands for: Hypermedia As The Engine Of Application State.

Two words are significant: Hypermedia and Application State.

Hypermedia is a combination of hypertext and media, that is, non-sequential text and media (audio, video) that connects its parts via links.

Application state is the state that the resource is in at any given moment. The state of a RESTful application is fully defined by it’s representation - the representation is the state of the resource. Because of that, a state diagram reflects the application by specifying all possible states and its edges2.

No prior knowledge.

A state diagram always has exactly one entry point. And this is what it makes so powerful: apart from the entrance URL, a client of an API does not need to have any other knowledge.

A RESTful API can be compared to a website. As a user, I only know the root URL of a website. Once I type in the URL (or click on the link) all further paths and actions are defined by other links. Those links may change at any moment, but as a user, all I need to know is the root URL and I’m still able to use the website.

Many REST APIs ignore HATEOAS. And this comes with a substantial cost of tight coupling.

The question is: how do we describe the links that lead from one state to another? And where do we put that information?

To answer the latter question, usually, two places are used: in the HTTP header or in the JSON payload.

HTTP defines a links section in its header: “The Link: header in HTTP allows the server to point an interested client to another resource containing metadata about the requested resource.”3. Putting links in the header has the advantage that they can be retrieved without accessing the actual resource (by calling HEAD on that resource). However, I prefer to put links in the JSON payload, because header information is metadata and links are an essential part of the REST API.

To further differentiate links from “normal” response fields, they often are prefixed by an underscore. So every JSON response of a resource has to contain a _links field describing the available paths from the current representation of the resource.

That leaves us with the last open question to answer: how do we actually describe a link? There are several ways conceivable, but in my opinion the most obvious way is to follow the HTTP header structure with a rel attribute that describes the relationship of the link and href.

For example, let’s assume I have developed a blogging API. All I know is the root URL and doing a GET on that URL returns following response:

GET /

HTTP/1.1 200 OK
{
    "_links" : [
        {
            "rel": "users",
            "href": "/users"
        },
        {
            "rel": "posts",
            "href": "/posts"
        }]    
}    

To retrieve a list of post, the client does not have to know anything about how the URLs are constructed. Instead, he follows the “posts” relation and retrieves the links from there.

GET /posts

HTTP/1.1 200 OK
{
    "posts": [
        {
            "message": "my first new post",
            "_links": {
                "rel": "self",
                "href": "/posts/1",
            }
        },
        {
            "message": "my second new post",
            "_links": {
                "rel": "self",
                "href": "/posts/2",
            }
        }
    ]    
    "_links" : [
        {
            "rel": "new",
            "href": "/posts",
        },
        {
            "rel": "self",
            "href": "/posts",
        }]    
}    

Only one relation is mandatory: self, which is a link to the resource itself. Following the self relation, the client can retrieve further operations on that resource.

Depending on the actual API, more attributes can be included, for example “method” and “content-type”. This depends on how “self-explanatory” the API and resources are.

In another post, I’m going to explain in more details how to design an HATEOAS constrained REST API, for now, I’d like to show how easy it is to test an API like that.

Testing an HATEOAS REST API

(Unit-)testing an API becomes much straight-forward if no prior knowledge about the URLs is required. The same advantage that goes for clients applies to tests.

Let’s write a test case for the blogging API. We create a new post and test whether it appears in the list of posts.

 
public class BlogApiTestCase
{
    @Test
    public void testPosting()
    {
        final ApiClient api = new ApiClient();
        
        // Assert that there are no posts in the system: get posts resource
        api.discovery().rel("posts").get();
         
        List<Map> posts = api.getJsonPath().getList("posts");
        assertThat(posts.size()).isEqualTo(0);

        // Create a new post: use "new" relation. Continue where we left off
        api.rel("new").post("{\"post\":{\"message\": \"A new blog post\"}}");

        // As above: now there should be one new post. Go back to entrance point ("discovery") => "posts"
        List<Map> newPosts = api.discovery().rel("posts").get().getJsonPath().getList("posts");
        assertThat(newPosts.size()).isEqualTo(1);
    }

}

The key advantage is that we can test the API the same way a real front-end client would use it, by navigating the links.

Another advantage of testing like that is that it automatically ensures your API is HATEOAS compatible - if you can’t test it by following links the API is not RESTful.

Extending RestAssured

I have created a tiny GitHub project4 that builds on top of RestAssured5. It consists of just one file, the test client class, and allows testing by navigating links.

An excerpt of that class (ApiClient):

import static com.jayway.restassured.*;
import static org.fest.assertions.Assertions.assertThat;

public class ApiClient
{
    private Response currentResponse;
    private String currentUrl;

    // ---------------------------------
    // Links relation
    // ---------------------------------

    public ApiClient rel(String rel)
    {
        this.currentUrl = getRelHref("", rel);
        return this;
    }

    public String getRelHref(String field, String rel)
    {
        if (!StringUtils.isEmpty(field))
        {
            field = field + ".";
        }
    
        final String path = field + "_links.find{_links -> _links.rel == '" + rel + "'}.href";
        logPath(path);
        String url = currentResponse.then().extract().path(path);
        logUrl(url);
    
        return url;
    }

    // ---------------------------------
    // Http requests
    // ---------------------------------

    public ApiClient URL(String url)
    {
        this.currentUrl = url;
        return this;
    }

    public ApiClient discovery()
    {
        currentResponse = url("/").get();
        return this;
    }

    public ApiClient post(String json)
    {
        currentResponse = given().contentType(ContentType.JSON).body(json).post(currentUrl);
        logResponse();
        assertOkResponse();
        return this;
    }

    public ApiClient get()
    {
        currentResponse = get(currentUrl);
        logResponse();
        assertOkResponse();
        return this;
    }
}

One of the key features we are taking advantage of is the usage of GPath6, which allows finding elements using expressions.

When we navigate through the links, most of the time we just want to follow the href value of a specific rel attribute (for example “posts”). In RestAssured, this can be done as follows:

_links.find{_links -> _links.rel == "posts"}.href

That returns href values of all links that have the relation rel=="posts".

Storing that url as currentUrl and also keeping track of the latest response of each call, it becomes straightforward to navigate along the links and testing the api on the way.

Have a look at the GitHub file (https://github.com/BernhardWenzel/hateoas-api-test-client) if interested. The client offers more methods for testing an API on the go (for example, if a response is an array of elements, pick one element based on a condition and continue navigating).

In a future post I’ll demonstrate how to use the test class to create more complex test cases, like that one:

api
 .discovery().rel("users").get()
 .filterRel("users", "name=='Tom.Hanks'", "self").get()
 .rel("user", "profilePictures").get()
 .rel("pictures", "pictures[0]").get()
 .rel("picture", "new").post("{'path': 'files/pic2.jpg'}")

Thanks for reading

I hope you enjoyed reading this post. If you have comments, questions or found a bug please let me know, either in the comments below or contact me directly.

Resources

Subscribe to Human Intelligence Engineering
Explorations of human ingenuity in a world of technology.