In the previous chapter, we introduced the concept of using REST Routes in our Endpoints class for the framework. In case if the route changes, we won't have to make changes everywhere, it's just at a single place in the Routes class.
Subsequently, in this chapter, we are going to implement the Generics concept in our framework. We are dealing with the response objects of various data types such as Token, Books, and User Account. Moreover, one can build these response objects fail-safe by using generics. Generics adds a layer of abstraction. In addition to that, they add a way to specify types for classes and methods. You can read more about the Generics concept in this Generics tutorial. In this article, we are going to cover:-
- What is the need for a Generics class implementation?
- Implementation of Generics
What is the need for a Generics class implementation
Consider an example of returning a Book object from Endpoint method like below:
public static Books getBooks() {
RequestSpecification request = RestAssured.given();
Response response = request.get(Route.books());
return response.getBody().as(Books.class);
}
If we would get a failed response, our code will fail in the above method implementation.
So supposedly if we were to handle the failure like below:
public static Books getBooks() {
RequestSpecification request = RestAssured.given();
Response response = request.get(Route.books());
int code = response.getStatusCode();
if( code == 200 || code == 201 || code == 202 || code == 203 || code == 204 || code == 205) {
return response.getBody().as(Book.Class());
}
}
we would not be able to test negative tests by sending a wrong body and expecting a 204. We would likely be stuck in such a case.
Thus we need a class, which will return the Response Body as well as status and in case of failure return exception or error message.
Now, the response received from the server can be of several data types. Hence, we need an interface capable of handling different response objects. To provide this parameterized value to a parameterized type, we implement this as a Generic Interface. This interface will contain all the methods we need when we operate on a REST Response.
Implementation of Generics
We will follow the steps below to implement Generics in our framework:
- Create a Generic Interface
- Create a class to implement generic interface methods
- Modification of the Endpoints class
- Modification of Steps class
- Run the tests
Create a Generic Interface
Firstly, right-click on the apiEngine package and select New >> Interface. Name it as IRestResponse.
IRestResponse.java
package apiEngine;
import io.restassured.response.Response;
public interface IRestResponse<T>{
public T getBody();
public String getContent();
public int getStatusCode();
public boolean isSuccessful();
public String getStatusDescription();
public Response getResponse();
public Exception getException();
}
Explanation:
We created a generic interface of type <T>
. Thus, we can use this interface to hold responses of different types. For example, IRestResponse<Books>
will hold the response of type Books. Likewise, it will be for IRestResponse<UserAccount>
and IRestResponse<Token>
.
Note: As explained above, we need this interface to return the Body as well as status, and in case of failure return exception or error message. Therefore, that's why we have those attributes in the interface.
Create a class to implement generic interface methods
We defined the methods we need to operate on the REST response. We need to apply these methods next in our Rest Response class.
Right-click on the apiEngine package and select New >> Class. Name it as RestResponse.
package apiEngine;
import io.restassured.response.Response;
public class RestResponse<T> implements IRestResponse<T> {
private T data;
private Response response;
private Exception e;
public RestResponse(Class<T> t, Response response) {
this.response = response;
try{
this.data = t.newInstance();
}catch (Exception e){
throw new RuntimeException("There should be a default constructor in the Response POJO");
}
}
public String getContent() {
return response.getBody().asString();
}
public int getStatusCode() {
return response.getStatusCode();
}
public boolean isSuccessful() {
int code = response.getStatusCode();
if( code == 200 || code == 201 || code == 202 || code == 203 || code == 204 || code == 205) return true;
return false;
}
public String getStatusDescription() {
return response.getStatusLine();
}
public Response getResponse() {
return response;
}
public T getBody() {
try {
data = (T) response.getBody().as(data.getClass());
}catch (Exception e) {
this.e=e;
}
return data;
}
public Exception getException() {
return e;
}
}
Explanation:
We implemented the methods to return us relevant details of REST responses as needed for testing.
- isSuccessful(): Will validate if the sending request was successful. It validates the response status code received against the numerous HTTP status codes denoting that request was successfully processed
- getResponse().getBody().asString(): We will need the response body content in String format at times. This method implementation takes care of it.
- getException(): In case our response body is not deserialized successfully, we will get an exception. e will contain this exception, which we get using this method.
Modification of Endpoints class
Our methods for Endpoints class will change. They will return responses of the type RestResponse<Books>
, RestResponse<Token>
and RestResponse<UserAccount>
for the respective methods.
For Example: Our method authenticateUser() is updated from:
public static Response authenticateUser(AuthorizationRequest authRequest) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.body(authRequest).post(Route.generateToken());
return response;
}
to
public static IRestResponse<Token> authenticateUser(AuthorizationRequest authRequest) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.body(authRequest).post(Route.generateToken());
return new RestResponse(Token.class, response);
}
Explanation:
In this method, we have wrapped the Rest Assured Response into our RestResponse class of the type Token, where we have deserialized the responses.
Similarly, we updated the below methods for the Endpoints class:
- getBooks()
- addBook()
- removeBook()
- getUserAccount()
Our updated Endpoints class will look likewise:
package apiEngine;
import apiEngine.model.requests.AddBooksRequest;
import apiEngine.model.requests.AuthorizationRequest;
import apiEngine.model.requests.RemoveBookRequest;
import apiEngine.model.responses.Books;
import apiEngine.model.responses.Token;
import apiEngine.model.responses.UserAccount;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
public class EndPoints {
private static final String BASE_URL = "https://bookstore.toolsqa.com";
public static IRestResponse<Token> authenticateUser(AuthorizationRequest authRequest) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.body(authRequest).post(Route.generateToken());
return new RestResponse(Token.class, response);
}
public static IRestResponse<Books> getBooks() {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.get(Route.books());
return new RestResponse(Books.class, response);
}
public static IRestResponse<UserAccount> addBook(AddBooksRequest addBooksRequest, String token) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json");
Response response = request.body(addBooksRequest).post(Route.books());
return new RestResponse(UserAccount.class, response);
}
public static Response removeBook(RemoveBookRequest removeBookRequest, String token) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json");
return request.body(removeBookRequest).delete(Route.book());
}
public static IRestResponse<UserAccount> getUserAccount(String userId, String token) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json");
Response response = request.get(Route.userAccount(userId));
return new RestResponse(UserAccount.class, response);
}
}
Modification of Steps class
We will modify the step definitions to call the methods listed in the endpoints class.
Moreover, you will directly obtain the response in the step definition class. As already explained, the logic of communication with the server and converting it into the response class moves out. Thus, our step definition consists of only the testing layer which we are interested in and not the internal workings of the API.
Our updated step definition file would look like:
package stepDefinitions;
import apiEngine.EndPoints;
import apiEngine.IRestResponse;
import apiEngine.model.*;
import apiEngine.model.requests.*;
import apiEngine.model.responses.*;
import org.junit.Assert;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import io.restassured.response.Response;
public class Steps {
private static final String USER_ID = "9b5f49ab-eea9-45f4-9d66-bcf56a531b85";
private static Response response;
private static Token tokenResponse;
private static IRestResponse<UserAccount> userAccountResponse;
private static Book book;
@Given("^I am an authorized user$")
public void iAmAnAuthorizedUser() {
AuthorizationRequest authRequest = new AuthorizationRequest("TOOLSQA-Test", "Test@@123");
tokenResponse = EndPoints.authenticateUser(authRequest).getBody();
}
@Given("^A list of books are available$")
public void listOfBooksAreAvailable() {
IRestResponse<Books> booksResponse = EndPoints.getBooks();
book = booksResponse.getBody().books.get(0);
}
@When("^I add a book to my reading list$")
public void addBookInList() {
ISBN isbn = new ISBN(book.isbn);
AddBooksRequest addBooksRequest = new AddBooksRequest(USER_ID, isbn);
userAccountResponse = EndPoints.addBook(addBooksRequest, tokenResponse.token);
}
@Then("^The book is added$")
public void bookIsAdded() {
Assert.assertTrue(userAccountResponse.isSuccessful());
Assert.assertEquals(201, userAccountResponse.getStatusCode());
Assert.assertEquals(USER_ID, userAccountResponse.getBody().userID);
Assert.assertEquals(book.isbn, userAccountResponse.getBody().books.get(0).isbn);
}
@When("^I remove a book from my reading list$")
public void removeBookFromList() {
RemoveBookRequest removeBookRequest = new RemoveBookRequest(USER_ID, book.isbn);
response = EndPoints.removeBook(removeBookRequest, tokenResponse.token);
}
@Then("^The book is removed$")
public void bookIsRemoved() {
Assert.assertEquals(204, response.getStatusCode());
userAccountResponse = EndPoints.getUserAccount(USER_ID, tokenResponse.token);
Assert.assertEquals(200, userAccountResponse.getStatusCode());
Assert.assertEquals(0, userAccountResponse.getBody().books.size());
}
}
Run the Cucumber Test
Run the Tests as JUnit
We are all set now to run the updated Cucumber test. Right -Click on TestRunner class and Click Run As >> JUnit Test. Consequently, you will see the result in the left-hand side project explorer window in the JUnit tab.
Run the Tests from Cucumber Feature
To run the tests as a Cucumber Feature, right-click on the End2End_Test.feature file. After that, select the Run As>>Cucumber Feature.
Our updated project folder structure of the framework will look likewise:
Our tests passed with the changes we made for the Generics implementation in our framework. We will be Refactoring for Request Headers so that we can make use of a single request object in our next chapter. Moreover, it will avoid the complexity of adding the auth header repeatedly for each request.
Meanwhile, please try to implement the above changes in your framework, as explained above.