REST Assured's Fluent Interface Design (Java)

Two topics that I haven't seen explained very well in the tutorials and videos that I've watched about the REST Assured testing library are:
  • Q: Why is its API designed the way it is? (A: It uses a Fluent interface design.)
  • Q: How does its Gherkin-like syntax actually work? (A: Its methods all return the interfaces most likely to be needed next.)
I'll explore these questions in more detail below.

This article assumes that you're already familiar with the basics of Java and have a general idea of what Behavior-Driven Development is and what test code that uses REST Assured looks like. For example:
given().
    param(...).
when().
    get(...).
then().
    statusCode(200);

Typical Java APIs

A typical object-oriented API in Java consists of:
  • a set of interfaces (or classes) that represent concepts related to the problem you're solving, and
  • methods on those interfaces that let you perform related actions.

Here's an example of a typical API in Java. Assume we have an application that deals with customers and customer orders, and assume that one order can contain multiple items. The application might provide an API that includes interfaces representing Customers, Orders, and Order Lines, and lets us create an order for a customer programmatically like this:
1  public void createAnOrder_Typical(Customer customer) {
2      Order order = new Order();                     // create a new, empty "order" object
3      customer.addOrder(order);                      // call the Customer's "addOrder" method to associate the new order with the given customer
4      OrderLine line1 = new OrderLine(5, "apples");  // create a new "order line" object for five apples
5      order.addLine(line1);                          // call the Order's "addLine" method to add the apples to the new order
6      OrderLine line2 = new OrderLine(3, "bananas"); // create a new "order line" object for three bananas
7      order.addLine(line2);                          // add the bananas to the order
8  }

This code creates all the objects we need and then wires them up together, one step at a time. Note that the addOrder and addLine methods on lines 3,5,7 here don't return a value.

Fluent APIs

An application with an API designed in a "Fluent" style might let you accomplish the same thing like this:
1  public void createAnOrder_Fluent(Customer customer) {
2      customer.newOrder().     // call the Customer's "newOrder" method to associate a new empty order with the given customer
3          with(5, "apples").   // add five apples to the new order
4          and().               // and
5          with(3, "bananas");  // add three bananas to the order
6  }

The goal of an API that's designed like this is to be readable and to flow. Notice that if you just read the code, it reads almost like English. The syntax in this "fluent" example looks different from the "typical" example above, but it doesn't actually use any new Java language mechanisms. This version just uses plain old interfaces and methods like the version above, but it uses them differently.

In this code, the newOrder() method that you call on the customer object returns another object that represents the newly-created order, and this returned Order object provides methods named with and and that you can call to build up the customer's order. The trick is that all the methods here return values, which the methods in the "typical" example above didn't do. So after you call the newOrder() method on the Customer object here on line 2, you can immediately call another method (with) on the Order object that newOrder() returned. And since with returns the newly-updated Order object too, you can immediately call another method on it, and so on.

A chain of method calls, where each method is invoked on the return value of the preceding method call, is called method chaining, and it's the technique that REST Assured uses to provide its Fluent API that lets you write BDD tests that read like Gherkin's Given-When-Then style.

Different people have different opinions about whether fluent interfaces in general are good or bad, and in what kinds of situations they make sense. Martin Fowler discusses this a bit in his FluentInterface article. The examples I gave above are simplified versions of his examples in this article.

A State Diagram

In a Fluent API, each method is invoked on an object, and always returns an object. So if we represent each method by an arrow drawn from its source object to its returned object, we can diagram the example API above like this:

This diagram says that when we have a Customer object, we can call the newOrder() method on it, and we'll get an Order object back. And when we have an Order object, we can call with(5,"apples") or and() on it and we'll get back the same Order object, ready for more method calls.

In this API, the and() method doesn't actually do anything. It just returns the Order object that you called it on. Its implementation would look like this:
public Order and() {
    return this;
}

This method's only purpose is to provide better flow when you're reading the source code. Calling it is entirely optional. REST Assured uses this technique in its API too. It provides and() (and a few other) methods that you can insert into your BDD method chain if you like without changing the meaning of your code at all. If you look at REST Assured's implementation, the source code for these methods is also just "return this;". In their documentation, they refer to these methods as "syntactic sugar." We'll see more of these below.

REST Assured's API

When you write a Gherkin-style test that uses REST Assured, there are three main phases:
  1. Preparing the HTTP request.
  2. Making the REST API call by sending the request and receiving the response.
  3. Checking the response.

Each of these phases is handled by a different REST Assured interface, and the typical given().when().get().then().assertThat()... code sequence moves through each of these interfaces in order.

An Overview Diagram

As an example, we'll consider the following code on the left. A simplified overview diagram of the REST Assured fluent API is on the right.
given().
    param("name1", "Alice").
    and().
    param("name2", "Bob").
when().
    get("/getBirthday/").
then().
    assertThat().
    statusCode(200);
We begin by calling given(). This method is static, which means we don't have to call it on an object; we can just call it directly. It returns a REST Assured RequestSpecification object that lets us start preparing our HTTP request. (More precisely, it returns an object that implements the RequestSpecification interface.)

The RequestSpecification object is responsible for preparing the HTTP request. It provides methods that let us specify properties of the request such as the base URI, parameters, content type, headers, and body. Each of these methods updates the RequestSpecification with the arguments we give it and then returns the updated RequestSpecification so we can call more methods on it.

The when() method actually does nothing. It's just syntactic sugar like the and() method we saw above. It helps us make our code read like Gherkin language syntax, but REST Assured doesn't actually require that we call it.

The RequestSpecification object is also used to make the actual RESTful API calls to the server using any of the HTTP request methods (GET, POST, etc.). In our example, we call .get("/getBirthday/"), which sends an HTTP GET request to the server using the request that we've prepared. The get(), post(), and related methods all send a request to the server and then return a REST Assured Response object that represents the HTTP response that the server returned.

The Response object provides methods to extract information about the HTTP response if you want to do that, but it doesn't actually provide any methods itself to do validation of the response. For that, it provides a then() method, which returns a ValidatableResponse object.

The ValidatableResponse object is responsible for performing validation (assertions) of the HTTP response. Its assertThat() method is just syntactic sugar that reads nicely between then() and the following assertion method calls. The ValidatableResponse provides methods to check the HTTP status code (which we do in this example), the headers, the response body, etc. These validation methods are described in other topics. Search for REST Assured in this wiki or on the Web for more info and tutorials.

Note that the set of methods that are available for us to call at any given point in our code depends on which particular interface we're dealing with at that point. So if we're dealing with a RequestSpecification object, we can call the param() method on it to set a request parameter, but we can't call statusCode() to check the response status code yet until we've made some other method calls and eventually gotten to the state where we're dealing with a ValidatableResponse object that provides this method. So a diagram like this is a way of illustrating the "grammar" that the fluent API allows. It shows which method calls are allowed to follow which other method calls.

A More Detailed Diagram

REST Assured's fluent API also lets you perform logging. For example, the syntax to write the body of an HTTP response to a log is .log().body(). When you call .log() on a ValidatableResponse object, it returns a new type of object, a ValidatableResponseLogSpec. At this point, the only methods that are available to call are methods that let you specify what kind of logging you want. For example, call .body() to log the response body, or call .headers() to log the response headers. These "log specification" methods return the ValidatableResponse object again, so they leave you back in the state you were in before you called .log(), and you can then continue validating the server's response with more assertion methods, or specify more logging.

REST Assured provides a similar kind of "detour" in its fluent grammar for specifying an authentication scheme to use in an HTTP request. When you're preparing your request and you call .auth() on the RequestSpecification, it returns an AuthenticationSpecification, which provides methods to specify an authentication scheme. These methods return back to the RequestSpecification so you can continue on from there. So for example, the code to specify a basic authentication scheme reads nicely: .auth().basic(userName, password). To explicitly specify that you want to use preemptive authentication, you can write .auth().preemptive().basic(userName, password). The API supports this option by detouring through a "PreemptiveAuthSpec" interface before returning back to the RequestSpecification.

Here's a more detailed diagram that includes some of the more commonly-used parts of REST Assured's fluent API:

-- BradChalfan - 17 Jul 2020
Topic revision: r5 - 17 Jul 2020, BradChalfan
© 2020 Ultranauts - 75 Broad Street, 2nd Floor, Suite 206, New York, NY 10004 - info@ultranauts.co