API Test Automation with REST-assured

Himanshu Bahuguna
Dotdash Meredith Tech Blog
7 min readFeb 13, 2019

--

Modern web applications commonly utilize web services which provide a web-based interface to database server. Representational State Transfer (REST) is an architecture for well-behaved web services that can function at Internet scale.

REST-assured is a Java DSL for simplifying testing of REST based web services built on top of HTTP Builder. It supports POST, GET, PUT, DELETE, OPTIONS, PATCH and HEAD requests and can be used to validate and verify the response of these requests.

REST-assured dependency

In order to use REST-assured in a test we will need to add it as a dependency to the project. We use maven in our projects, so we have added the following REST-assured dependency to our POM:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.1.0</version>
</dependency>

REST-assured depends on Hamcrest to perform assertions, therefore we must add Hamcrest dependency to our project as well.

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
</dependency>

Static Imports

In REST-assured tests, its better to use static imports, because it makes test readable.

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.equalTo;
@Test
public void restAssuredExample {
given().param("key","value").
when().contentType("application/json").
get("/endPoint").then().
statusCode(200).assertThat().
body("data.key",equalTo(expected));
}

REST-assured Config

We can set a global config for our tests. This can be done in a class that can be accessed by all tests. For our tests we are using a Singleton class so that REST-assured config is initialized once per regression test run. This way we can create a base URL that can be used by all tests.

We are using Gson as the object mapper in our tests. And we can tell REST-assured to enable logging when a test fails. For this we need to set the following values when initializing Rest-assured config :

RestAssured.config = RestAssuredConfig.config() .objectMapperConfig(new ObjectMapperConfig(GSON)) .logConfig(new LogConfig().enableLoggingOfRequestAndResponseIfValidationFails());

Similarly, we can build the base url in our Configuration class and use it in the tests by setting the following configuration:

RestAssured.baseURI = baseUri;

Using POJOs

Json strings to create and fetch data are error prone. So we are using POJOs that represent our APIs and the responses. In most of the cases, the same POJO can be used for serialization and deserialization. One advantage of using Gson is that we dont have to tell object mapper to ignore unwanted fields explicitly.

Using Lombok

We are using Lombok to create POJOs. Lombok provides the capability to have getters and builders for a POJO without actually writing them in the class. We just need to provide @Getter and @Builder annotations to the class. Getters are used when deserializing the response and Builders are used to create data for the tests. Below is an example:

import lombok.Builder;
import lombok.Getter;

@Builder @Getter
public class Image {
private String id;
private String objectId;
private String url;
private String caption;
private String owner;
private String alt;
private Integer width;
private Integer height;
private String fileName;
private String authorId;
private String user;
}

We can use the above mentioned Image class to create data that can be used by REST-assured in a POST method. For this we use Supplier function so that the same method can be reused :

Data Creation

REST-assured supports specifying an Object request content that can automatically be serialized to JSON or XML and sent with the request. Therefore, rather than having json in text files we can build the Objects that can be built using the POJOs containing Lombok’s builder mechanism. Basically, we can have some classes that contain static methods to supply a particular Object. Below is one such example to build Image data using the Image POJO mentioned above:

public static Supplier<Image> image1() {
return () -> Image.builder()
.url("http://Lighting-in-brain.jpg")
.caption("test image caption1")
.owner("GettyImages")
.alt("test1")
.width("202")
.height("280")
.fileName("testFile1")
.authorId("123456")
.user("test1")
.build();
}

If we dont want to save the image Object on the heap, then we can use Supplier, which will generate a new Object every time the image1() method is called. However, we can use Lazy initialization if we want to actually reuse an Object. Below is an example of Lazy initialization of Objects:

public class Lazy<T> {
private final Supplier<T> supplier;
private T instance;

public Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}

/**
*
@return Lazily constructed instance.
*/
public T get() {
if (instance == null) {
instance = supplier.get();
}
return instance;
}
}

When we call the get() method the very first time, the Object is created and saved to the instance variable. From the next time get() method will return the Object saved into the instance variable.

public static Lazy<Image> image2() {
return new Lazy(() -> Image.builder()
.url("http://young-leaves-poison-ivy-big.jpg")
.caption("test image caption 2")
.owner("GettyImages")
.alt("test2")
.width("202")
.height("280")
.fileName("testFile2")
.authorId("123456")
.user("test2")
.build());
}

Creating Client For Services

We can create separate client for each service. We plan to create and destroy the data with each test so that each test has its own data. In general, its better to have test create on its own data because that way its less brittle. So we can have a contract in our Service clients to create and delete data. There is an advantage to this policy, that we can have our service level clients implement the interface. This allows us to call client specific delete method at runtime from a generic test method. We can use this approach to avoid writing some boilerplate code to delete the data that was created for a test.

For an example, we can write an ImageClient that implements a RestAssuredClient. RestAssuredClient requires its classes to implement create() and delete() methods. Now, we can write our generic test method that takes a Consumer<Stack<RestAssuredClient>> lambda. This lambda is the actual test code we want to run. We will populate <Stack<RestAssuredClient> in the test whenever we create data. Once the test is done, we can pop the RestAssuredClient Objects and call their delete() method. Test will determine on runtime which delete() method to call based on the actual class that was used to create data. Below is an example:

public interface RestAssuredClient {
/*
* creates a document
* should be used to post
*
*/
Response create(Status status);

/*
* deletes a document
* should be used to delete
*
*/
Response delete(Status status);

}
public class ImageClient implements RestAssuredClient {

private Response imageSaveResponse;

@Override
public Response create(Status status) {
File imageFile = new File("test"+ new Random().nextInt() +".jpg");
InputStream is = FileUtils.class.getResourceAsStream("/datasource/rest-assured/image/test.jpg");
try {
Files.copy(is,imageFile.getAbsoluteFile().toPath());
} catch (IOException e) {
throw new IllegalStateException("image copy failed!");
}
return imageSaveResponse = given().spec(RequestSpec)
.multiPart("json", ImageData.image1().get())
.multiPart("file", imageFile)
.when().post("/image");
}

@Override
public Response delete(Status status) {
return given().spec(SeleneRequestSpec.getInstance())
.param("id", imgResponse.getData().getId())
.when()
.delete("/image/metadata");
}
}
protected void test(Consumer<Stack<RestAssuredClient>> test) {
Stack<RestAssuredClient> deleteClients = new Stack<>();
try {
test.accept(deleteClients);
} catch(Exception e) {
throw e;
} finally {
while(!deleteClients.isEmpty()) {
deleteClients.pop().delete(OK);
}
}
}
@Test
public void testImage(){
test((clients) -> {
ImageClient imageClient = new ImageClient();
imageClient.create(OK);
clients.push(imageClient);
});
}

In the above test an Image will be posted, but we wont need to write code to delete image in the test. We can use this same pattern for all tests that need to create data.

Request Builder

When we want to fetch some data using a Get request, we usually have some optional query parameters that can be passed to the request. We can achieve request creation by using builder pattern. This way we can build our request with only the query parameters that are required by the test.

@Getter @Builder
public class ImageRequest {
private String projection;
private String id;
}

Deserializing Response

We can add a read() method to our ImageClient. This method takes an ImageRequest and returns an Image Object based on the request. REST-assured provides us with extract().as() method that takes a class and deserializes the get() request’s response to the Image class. Below is an example:

public Image read(ImageRequest request) {
return given().spec(requestSpec)
.param("id", request.getId())
.param("projection", request.getProjection())
.when().contentType("application/json")
.get("/image")
.then().statusCode(200)
.extract().as(Image.class);
}

Asserting Values From Response

Once we receive our response back as Image Object, we can use getters in the Image class to assert certain fields in the response.

@Test
public void testImage(){
test((clients) -> {
ImageClient imageClient = new ImageClient();
Image img = imageClient.create(OK).as(Image.class);
clients.push(imageClient);
ImageRequest request = ImageRequest.builder().id(img.getId).build();
Image imgGetResponse = imageClient.read(request);
assertThat(imgGetResponse.getId(), is(img.getId));
});
}

Using SameBeanAs Matcher

If we want to compare our response to match more than one fields, we can use SameBeanAs matcher from shazamcrest library. This matcher will match if the 2 beans are exactly same or not. We can store our expected values in an Object and later use this to match the entire response at once. In most cases we can use the builder method from the data that was used to create the data to post. In get request we can assert that the image that was posted has exactly same fields that were posted. Below is an example:

@Test
public void testImage(){
test((clients) -> {
ImageClient imageClient = new ImageClient();
Image img = imageClient.create(OK).as(Image.class);
clients.push(imageClient);
ImageRequest request = ImageRequest.builder().id(img.getId).build();
Image imgGetResponse = imageClient.read(request);
assertThat(imgGetResponse, is(sameBeanAs(ImageData.image1().get())));
});
}

REST-assured provides us with a lot of features and we have just started using it for our API test automation. We hope we will be able to improve our API test automation with the help of REST-assured. So far it has enabled us to write better tests, hope readers can also benefit from some techniques that we have used in our REST-assured based API automation framework. Thanks!

--

--