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 blog1 “What 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.
Links
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.
Links in the HTTP header
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.
Links in the JSON payload
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.
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
):
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:
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.