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:
- Preparing the HTTP request.
- Making the REST API call by sending the request and receiving the response.
- 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