Introduction
Having a large collection of unit tests that verify the behaviour of Java classes is only the first step to a sound testing strategy. After all, the fact that individual Java classes work successfully in isolation does not mean that the application itself will also work correctly ,when all these classes are bundled together.
In addition to basic unit tests, we also need integration tests (tests that focus on modules), functional tests (end-to-end tests that use the application as deployed), and even user acceptance tests (tests that examine the GUI as seen by the user).
In this tutorial, we will deal with functional tests that do not work directly with Java classes. Instead, they connect to the HTTP endpoints offered by the application server and mimic the role of another client or the browser.
Most applications today expose their API as a set of HTTP endpoints that send and receive JSON data. These endpoints can be used either from the GUI layer (i.e. Javascript front-end frameworks) or other back-end applications in different technologies. Making sure that these HTTP endpoints work according to the specifications is an essential requirement if we want to cover the whole development lifecycle and follow the testing pyramid paradigm correctly.
We will cover:
- Downloading and setting up REST Assured – a Java library for testing HTTP endpoints,
- Simple tests that perform one interaction with the HTTP endpoint,
- More complex functional tests that require a “conversation” between the client and the HTTP endpoint, and
- Different ways of posting requests and evaluating responses.
Note that REST Assured treats our REST application as a Black box during testing. All REST Assured tests send a network request to our application, get a response back, and compare it against a predetermined result.
The fact that the application under test is written in Java is irrelevant to REST Assured. REST Assured bases its tests only on JSON and HTTP, which are language-independent technologies. We can use REST Assured to write tests for applications written with Python, Ruby, .NET etc.
As long as our application offers an HTTP endpoint, REST Assured can test it regardless of the implementation programming language. It is convenient for us to use REST Assured for Java applications, as we’ll use the same language both for implementation and our unit tests.
Prerequisites
It is assumed that we already have a Java project that offers its services over HTTP/JSON when deployed to an application server. We will need:
- A sample Java project that already has an HTTP/REST/JSON API,
- A valid
pom.xml
file that builds the project, - Maven installed (the command
mvn
should be available in your command line), and - Internet access to download Maven dependencies.
REST Assured works on top of JUnit, therefore JUnit knowledge is essential. Hamcrest knowledge is helpful but not required as we will only use some basic Hamcrest matchers.
It is also assumed that we already know our way around basic Maven builds. If not, then feel free to consult its official documentation first.
Introduction to REST Assured
REST Assured is a Java library for validation of REST web services. It offers a friendly DSL (Domain specific Languages) that describes a connection to an HTTP endpoint and expected results.
Comparing REST Assured to Other REST Java Libraries
There are many Java libraries that allow us to write a REST client. It is also possible to use a simple HTTP client library and manually (de)serialize JSON data from/to the server.
We could use a client library like Jersey or the Spring REST template to write REST unit tests. After all, it is very logical to use the same Java library both on the client and on the server side for the sake of simplicity.
However, REST Assured has an additional testing DSL on top of its REST client that follows the BDD paradigm, making tests very readable.
Here is a trivial example:
import static com.jayway.restassured.RestAssured.given;
import org.junit.Test;
public class HelloWorldRestAssured {
@Test
public void makeSureThatGoogleIsUp() {
given().when().get("http://www.google.com").then().statusCode(200);
}
}
This JUnit test connects to Google, performs a GET call and makes sure that HTTP code 200/success
is returned. Notice the complete absence of the usual JUnit assert statements. REST Assured takes care of this for us and will automatically pass/fail this test according to the error code.
The flexibility of the given()
, when()
, then()
methods will become apparent in the following sections of the tutorial.
Setting up REST Assured
Using the REST Assured library in a Java project is very straightforward, as it is already a part of Maven central. We need to modify our pom.xml
file as follows:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>2.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
REST Assured works on top of JUnit and therefore assumes that it’s installed. Hamcrest Matchers are optional, and are not strictly needed for REST Assured. They allow us to write more expressive unit tests. Gson is automatically used by REST Assured for JSON (de)serialization, as we will see in the examples. Rest Assured can also work with Jackson, if that is available in the classpath.
With the compile time dependencies out of the way, we should also define a base Java class that will allow us to select the server that contains the REST endpoints. We can name this class as we see fit, but it is important since it will be used as a base class for all REST Assured tests.
public class FunctionalTest {
@BeforeClass
public static void setup() {
String port = System.getProperty("server.port");
if (port == null) {
RestAssured.port = Integer.valueOf(8080);
}
else{
RestAssured.port = Integer.valueOf(port);
}
String basePath = System.getProperty("server.base");
if(basePath==null){
basePath = "/rest-garage-sample/";
}
RestAssured.basePath = basePath;
String baseHost = System.getProperty("server.host");
if(baseHost==null){
baseHost = "http://localhost";
}
RestAssured.baseURI = baseHost;
}
}
This class prepares REST Assured with a default target of http://localhost:8080/rest-garage-sample which is the context root of the application we are going to test.
At the same time, it also allows us to set up a different web context using command line arguments. For example, to change this to another server and port we could run.
mvn test -Dserver.port=9000 -Dserver.host=http://example.com
This will run the same suite of tests for an application deployed at http://example.com:9000/rest-garage-sample. Thus a build server could run our REST Assured test against different environments.
HTTP API of a Sample Java Application
For this tutorial, we will test a sample REST application that deals with a garage. The garage has 150 spaces for cars. The API can be summarized as follows:
- GET call at /garage returns information of filled/free car parking slots,
- POST call at /garage/slots parks a new car at the next free parking slot,
- PUT call at /garage/slots/{slotID} parks a car at a specific parking slot,
- GET call at /garage/slots/{slotID} gives information on the availability of a specific slot, and
- DELETE call at garage/slots/{slotID} empties that slot (the car leaves the garage).
All input and output of these calls is based on the JSON format. The REST client sends data to these endpoints in JSON format and the responses it gets are also in the JSON format. REST Assured also supports XML but for this tutorial we will focus on JSON.
Testing HTTP Error Codes
We’ll start with REST Assured by verifying the HTTP error codes of our application.
First of all, we want to verify that our application has deployed correctly by calling the /garage URL and making sure that the success – 200 HTTP result is returned.
Here is the unit test:
public class GarageRestTest extends FunctionalTest{
@Test
public void basicPingTest() {
given().when().get("/garage").then().statusCode(200);
}
}
Here, we extended the FunctionalTest
class we’ve created before in order to define the context root of our application. This makes the test more readable as only the actual REST endpoint is contained in the test.
Running this test will make sure that our application is up and running.
Testing HTTP error codes becomes very useful when we want to make sure that our application behaves correctly, even when the input data is wrong. We already know that our garage has only 150 parking spaces. Therefore, a query to a parking space that exceeds this number should probably return a 404 (not found) error. Here is the respective code:
@Test
public void invalidParkingSpace() {
given().when().get("/garage/slots/999")
.then().statusCode(404);
}
Now that we know the basics, we can start verifying the body of HTTP responses as well.
Testing GET Calls and Their Responses
The majority of our REST functional tests will probably be simple GET calls that will make sure that the application is in a valid state. REST Assured offers several ways to examine the content of the response body.
A basic call to the top level URL of our service (i.e. /garage) returns the following JSON object:
{
"name":"Acme garage",
"info":{
"slots":150,
"status":"open"
}
}
The object holds the name of the garage and the number of total parking positions.
The simplest way to test the body of a network response is by using string comparison. For example, to verify the name of our garage we can write the following:
@Test
public void verifyNameOfGarage() {
given().when().get("/garage").then()
.body(containsString("Acme garage"));
}
This example shows how well REST Assured works with Hamcrest matchers. The body()
method is provided by REST Assured and deals with whatever is returned from the call. The containsString()
method comes from Hamcrest and makes the test pass (or fail) if the body contains that string.
The fact that REST Assured has built-in support for Hamcrest matchers means that we can write unit tests that closely resemble English sentences. This makes REST Assured tests very readable. It is possible to use REST Assured without Hamcrest matchers, but then our tests need more Java code for assertions.
Here’s how we can test the response of the top URL in a more structured way:
@Test
public void verifyNameStructured() {
given().when().get("/garage").then()
.body("name",equalTo("Acme garage"));
}
Here, we explicitly tell REST Assured that we want to verify the name property of the JSON object to its exact value. Reading this test feels very natural because it is very close to an English sentence: “When we get /garage
, then the response body should have a name property which is equal to Acme garage”.
Internally, REST Assured uses Groovy and allows for Groovy expressions inside its API. Teaching the whole Groovy syntax is outside the scope of this tutorial. Suffice to say that Groovy allows us to access a JSON object using the standard dot notation as if it were a Java object. This allows us to test the inner part of the JSON object just by specifying its path inside the JSON object
@Test
public void verifySlotsOfGarage() {
given().when().get("/garage").then().
body("info.slots",equalTo(150))
.body("info.status",equalTo("open"));
}
When given a path like info.slots
, REST Assured will search for a info
property inside the JSON object, follow it and then get a slots
property which will finally be used for the unit test. This is a convenient way to examine only the part that we are interested in out of a big JSON object.
Another thing to notice is the chaining of multiple body methods. This is one of the big advantages of the syntax of REST Assured as it makes possible multiple checks to work in unison. This unit test will succeed only if both checks on the response body are successful.
In fact, we can chain any type of verifications offered by REST Assured together:
@Test
public void verifyTopLevelURL() {
given().when().get("/garage").then()
.body("name",equalTo("Acme garage"))
.body("info.slots",equalTo(150))
.body("info.status",equalTo("open"))
.statusCode(200);
}
Here, not only do we check all the properties of the JSON object, but we also want to make sure that the HTTP error code is 200 as shown in the previous section.
Sending Test JSON Data with POST Calls
We now know how to verify the READ operations of a REST API. Several REST endpoints also offer the ability to send data, via POST or PUT calls. REST Assured supports WRITE operations as well.
A car enters the garage when its details are posted at /garage/slots.
The JSON object expected by the service is the following:
{
"plateNumber":"xyx1111",
"brand":"audi",
"colour":"red"
}
We need a way to create this JSON request. In its simplest form, REST Assured can create JSON objects from plain Java maps. This makes posting data very straightforward:
@Test
public void aCarGoesIntoTheGarage() {
Map<String,String> car = new HashMap<>();
car.put("plateNumber", "xyx1111");
car.put("brand", "audi");
car.put("colour", "red");
given()
.contentType("application/json")
.body(car)
.when().post("/garage/slots").then()
.statusCode(200);
}
Here, we’ve created a simple Java map and filled it with the values that represent JSON properties. Sending it with REST Assured requires the .contentType()
method, but other than that, the map is passed directly to the body()
method and REST Assured makes the conversion automatically to a JSON object.
The response of the call is the status of the parking space.
{
"empty":false,
"position":26
}
This sample response shows us that parking slot with number 26 is now occupied. We can verify the “position” and “empty” properties using the chaining of body()
methods, as already shown in the previous section.
@Test
public void aCarGoesIntoTheGarageStructured() {
Map<String,String> car = new HashMap<>();
car.put("plateNumber", "xyx1111");
car.put("brand", "audi");
car.put("colour", "red");
given()
.contentType("application/json")
.body(car)
.when().post("/garage/slots").then()
.body("empty",equalTo(false))
.body("position",lessThan(150));
}
The lessThan
Hamcrest matcher will fail the test if the position
property is ever above 150.
Using Java maps as the payload of a request means that we can create with them any JSON object on the spot for a specific test. For bigger JSON objects, an alternative solution would be to directly use a Java object. This makes the intent of the test very clear and also allows for more flexible verifications.
First, we need to define the car object in Java:
public class Car {
private String plateNumber;
private String colour;
private String brand;
[...getters and setters removed for brevity...]
}
Now, we can construct a car object in a type safe manner and use that for our test instead of the Java map:
@Test
public void aCarObjectGoesIntoTheGarage() {
Car car = new Car();
car.setPlateNumber("xyx1111");
car.setBrand("audi");
car.setColour("red");
given()
.contentType("application/json")
.body(car)
.when().post("/garage/slots").then()
.body("empty",equalTo(false))
.body("position",lessThan(150));
}
The REST Assured methods are exactly the same as with the previous test. We’ve only changed the data rerquest to a Java object. In this trivial example, having a Java object instead of a map may not have clear advantages. In a real enterprise project where the JSON payload will be larger, it will be far easier to deal with objects instead of maps.
Also, if our server code is already in Java, we can re-use the model objects directly from its source code. In our example, there is a very high possibility that the server code already contains the file Car.java
and therefore it can be copied/imported to the source code of the REST Assured tests without any additional effort.
Now that we have the car object, it is also very easy to look at the whole JSON response object. REST Assured can deserialize JSON data to Java objects in a similar manner:
Again, we define a new Java object from the return result:
public class Slot {
private boolean empty;
private int position;
[...getters and setters removed for brevity...]
}
Now, we can use Java objects for both the request and the response of the call.
@Test
public void aCarIsRegisteredInTheGarage() {
Car car = new Car();
car.setPlateNumber("xyx1111");
car.setBrand("audi");
car.setColour("red");
Slot slot = given()
.contentType("application/json")
.body(car)
.when().post("/garage/slots")
.as(Slot.class);
assertFalse(slot.isEmpty());
assertTrue(slot.getPosition() < 150);
}
With the Slot object, we now have full control over the assert statements that test it and can write the usual JUnit verifications.
Reusing Data from a Previous Call to the Next One
All examples we have seen so far are self-contained in the sense that a single call is performed to the server and only a single response is evaluated. However, we need to examine a sequence of events and create calls that depend on the previous ones several times.
In our sample application we need to test the event where a car leaves the garage. This is accomplished with a DELETE call at the /garage/slots/<slotNumber>
URL.
REST Assured can work with dynamic URLs as seen below:
@Test
public void aCarLeaves() {
given().pathParam("slotID", 27)
.when().delete("/garage/slots/{slotID}")
.then().statusCode(200);
}
Our test creates a URL that specifies parking position 27 using the .pathParam()
method. The problem with this unit test is its uncertainty. By the time it runs, position 27 might be empty or not. It’s possible that another test has already unparked the car.
Rather than guessing and making the result of the test non-deterministic, it is a good practice to park the car ourselves instead, and use the returned position for the DELETE call. This way, the exact number of the parking space becomes irrelevant, and the test can work with ANY position assigned at the time or running.
REST Assured can extract information from a response while still verifying the call on its own. Here is a deterministic test that does not depend on the number of the slot assigned to the incoming car:
@Test
public void aCarEntersAndThenLeaves() {
Car car = new Car();
car.setPlateNumber("xyx1111");
car.setBrand("audi");
car.setColour("red");
int positionTakenInGarage = given()
.contentType("application/json")
.body(car)
.when().post("/garage/slots").then()
.body("empty",equalTo(false))
.extract().path("position");
given().pathParam("slotID", positionTakenInGarage)
.when().delete("/garage/slots/{slotID}").then()
.statusCode(200);
}
This test has two REST calls. The first one is verified with the .body()
method, but at the same time the .extract()
method keeps the slot number.
This slot number is then reused in the DELETE call, so that we unpark the car that has just entered. The garage can allocate any number to our car and the test is no longer hard-coded to use slot with number 27.
Integrating REST Assured Tests in the Build Process
REST Assured uses JUnit, so testing support in the build process is ready out of the box for the tools that support JUnit. It should be evident that REST Assured tests expect the application to be deployed, as they hit directly the HTTP endpoints.
This makes REST tests different than plain unit tests, as they have requirements that are more complex than tests which depend only on Java source code. For a detailed tutorial on how to run REST tests in the build pipeline see the previous tutorial on splitting JUnit tests.
Conclusion
In this tutorial, we have written unit tests for the REST endpoints of a sample application using the Java library REST Assured.
We have seen:
- How to download and setup REST Assured via Maven,
- How to make REST Assured test configurable so that we can select the host/port they check,
- How to write basic unit tests that check HTTP error codes,
- How REST Assured tests can send data in JSON format using Java maps,
- How REST Assured tests can send data in JSON format using Java model classes,
- How REST Assured tests can verify the response data with Hamcrest matchers,
- How REST Assured tests can extract response information for further validations, and
- How we can use REST Assured for tests that require multiple REST calls.
Where to Go from Here
We have just scratched the surface of REST Assured. There are several more features that can be used for REST testing:
- Validating JSON responses according to a schema,
- Working with XML data instead of JSON,
- Using Groovy closures or Java 8 lambdas for validating responses,
- Working with HTTP headers,
- Working with cookies,
- Posting form parameters,
- Using timeouts in tests,
- Testing different authentication methods and SSL, and
- Writing custom (de)serializers.
Even if our application has some special needs, there is a good chance that REST Assured has explicit support for that corner case or custom implementation.
We hope that you now have a better understanding of REST Assured. If you have any questions or concerns, post them below. Feel free to share this tutorial with anyone you think might benefit from it.
Hello, thanks for sharing how do you use REST Assured framework. I was wondering if you could also check the REST testing framework WebTau: https://github.com/testingisdocumenting/webtau