We learned to convert JSON Request Body as well as JSON Response Body to Java Object in the previous chapters. We used Serialization and Deserialization concepts to achieve this. It led to our tests becoming more maintainable and less error-prone. Now in this chapter, we will work on the Separation of Test Layer with API Services and take a step further to make our tests cleaner and maintainable.
If you look at the Steps class, which is our test layer, you would see that we have a lot of logic that isn't actually related to testing. It concerns communicating with the server with a given test request and getting a response. Refer to the screenshot below:
In actual projects, we have a large number of APIs and tests implemented. They all contain the same logic of communicating with the server in every test, like:
- Buiding RequestSpecification object
- Adding headers
- Making calls to the server
This leads to a lot of code duplication in every test file. Additionally, if tomorrow there are some changes required in a particular endpoint, we will need to make changes in several places. It is not advisable because it makes our framework less maintainable.
Additionally, the test layer needs to focus only on the test data (parameters) sent in the request and receive responses from the APIs. It should not be focused on the heavy logic of the internals of API implemented. So, as software testers, we are only interested in the request and response obtained for these requests. Moreover, the test data sent in the request generally pass from the feature file in the Cucumber framework. This test data can be parameters to form a request body, request header or request resources.
As we have seen in our first chapter Understanding API Documentation, endpoints indicate ways to access resources from the server. Thus, we will essentially combine all the logic of endpoints from our Steps file and move it to a common class. The class will contain methods that take the required request parameters and send back a response received from the server. The endpoints should always get the request body from the test layer. In this article, we are going to cover:-
- Separation of Test Layer with API Services
- Create API Services for the Test
- Create API Services
- Add methods of API Services
- Call API Services in the Step Definitions(test layer)
- Run the tests
Separation of Test Layer with API Services
We abstract the logic of communication with the server into a separate class. It will make our Steps class cleaner. Subsequently, as we progress to write more steps into Step-Definition files in the framework, for different scenarios, we can reuse this logic of communicating with the server.
Consider the example to generate a token request of our Book Store API: /Account/v1/GenerateToken
For writing tests to test the generate token, our tests should focus the response obtained for the request body passed. In addition to this, they should not focus on internal implementation for the API. In a project with a large number of APIs, it is better to separate the Endpoints from Steps file.
Moreover, if we send correct credentials, we get a success response as a result. Refer below screenshot:
But, if we send invalid credentials, we will get an error response.
Thus, as testers, the focus is on the parameters sent in the body of the request and response we get. It is as same as what we are doing at the Book Store API in Swagger Tool above. We do not have a clue how swagger is making use of HTTP and how it is communicating with the server. The same way the test layer should be agnostic, the fact of how the service layer is handling the request. Additionally, a test layer should only focus on parameters passed to the request, whether it is query parameter, header parameters, or a body parameter, etc.
Let's see how to separate the Service layer from the Test layer.
Create API Services for the Test
Now we are going to create API Services, which will be consumed by the test layer, and this way, the test layer will be clean and efficiently focus only on test parameters.
- Firstly, create API Services
- Secondly, add methods of API Services
- Thirdly, call API Services in the Step Definitions(test layer)
- Fourthly, run the tests
Create API Services
- Firstly, Right-click on the apiEngine package and select Class. Additionally, name it as Endpoints. It would be our API Services class.
package apiEngine;
public class Endpoints {
}
We are moving all our endpoints to the Endpoints class. It is rational to move the BASE URL as well to the Endpoints class from Steps file. Moreover, it will help us to refer to the BASE URL when sending requests to our endpoints.
Thus, the updated Endpoints class will look likewise:
package apiEngine;
public class Endpoints {
private static final String BASE_URL = "https://bookstore.toolsqa.com";
}
Add methods of API Services
Firstly, we will extract the way to authenticate the user from Steps into Endpoints class. Additionally, we shall pass the object of *AuthorizationRequest *to the method.
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("/Account/v1/GenerateToken");
return response;
}
Code Explanation:
As you can see, we have created a method authenticateUser. In this method, we pass user credentials through AuthorizationRequest object, authRequest in the Request. Moreover, we are passing the BASE_URL and the headers as part of the request as well.
Similar to the above authenticateUser() method, we will create methods for :
- getBooks()
- addBook()
- removeBook()
- getUserAccount()
Putting all the methods together for the Endpoints class:
Endpoints.java class
package apiEngine;
import apiEngine.model.requests.AddBooksRequest;
import apiEngine.model.requests.AuthorizationRequest;
import apiEngine.model.requests.RemoveBookRequest;
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 Response authenticateUser(AuthorizationRequest authRequest) {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.body(authRequest).post("/Account/v1/GenerateToken");
return response;
}
public static Response getBooks() {
RestAssured.baseURI = BASE_URL;
RequestSpecification request = RestAssured.given();
request.header("Content-Type", "application/json");
Response response = request.get("/BookStore/v1/Books");
return response;
}
public static Response 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("/BookStore/v1/Books");
return 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");
Response response = request.body(removeBookRequest).delete("/BookStore/v1/Book");
return response;
}
public static Response 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("/Account/v1/User/" + userId);
return response;
}
}
We now move to our next step. In the next step, we will replace the endpoints in the Steps file with our newly created methods.
Talk with API Services in the Step Definitions
We have abstracted the logic of communication with the server into an Endpoint class. It will now help us make our Steps class cleaner as we progress into replacing the endpoints with the associated methods. In addition to this, we will be reusing this logic of communicating with the server.
Thus, the first part of the Steps class, with changes in the declared variables will be:
public class Steps {
private static final String USER_ID = "9b5f49ab-eea9-45f4-9d66-bcf56a531b85";
private static Response response;
private static Token tokenResponse;
private static Book book;
For the step definition:
@Given("I am an authorized user")
is updated as:
@Given("I am an authorized user")
public void iAmAnAuthorizedUser() {
AuthorizationRequest authRequest = new AuthorizationRequest("TOOLSQA-Test", "Test@@123");
response = EndPoints.authenticateUser(authRequest);
tokenResponse = response.getBody().as(Token.class);
}
We will likewise modify the rest of our step definitions. Moreover, we put all these changes for the Step Definitions together into the Steps file.
The updated Steps file will look likewise:
package stepDefinitions;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import apiEngine.Endpoints;
import apiEngine.model.Book;
import apiEngine.model.requests.AddBooksRequest;
import apiEngine.model.requests.AuthorizationRequest;
import apiEngine.model.requests.ISBN;
import apiEngine.model.requests.RemoveBookRequest;
import apiEngine.model.response.Books;
import apiEngine.model.response.Token;
import apiEngine.model.response.UserAccount;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.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 Book book;
@Given("I am an authorized user")
public void iAmAnAuthorizedUser() {
AuthorizationRequest authRequest = new AuthorizationRequest("TOOLSQA-Test", "Test@@123");
response = Endpoints.authenticateUser(authRequest);
tokenResponse = response.getBody().as(Token.class);
}
@Given("A list of books are available")
public void listOfBooksAreAvailable() {
response = Endpoints.getBooks();
Books books = response.getBody().as(Books.class);
book = books.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);
response = Endpoints.addBook(addBooksRequest, tokenResponse.token);
}
@Then("The book is added")
public void bookIsAdded() {
Assert.assertEquals(201, response.getStatusCode());
UserAccount userAccount = response.getBody().as(UserAccount.class);
Assert.assertEquals(USER_ID, userAccount.userID);
Assert.assertEquals(book.isbn, userAccount.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());
response = Endpoints.getUserAccount(USER_ID, tokenResponse.token);
Assert.assertEquals(200, response.getStatusCode());
UserAccount userAccount = response.getBody().as(UserAccount.class);
Assert.assertEquals(0, userAccount.books.size());
}
}
Run the Test
Run the Tests as JUnit
We are all set now to run the updated Cucumber test. Firstly, Right -Click on TestRunner class and Click Run As >> JUnit Test. Cucumber will run the script in the same fashion as it runs in Selenium WebDriver. Finally, the result will display 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.
Moreover, our updated project folder structure of the framework will look likewise:
In the next chapter, we will introduce the Implementation of Routes in Endpoints in our API automation framework. That would help us to make the framework maintainable and we won't have to make changes everywhere when the route changes except for Routes class. Additionally, please implement the Endpoints class as discussed above, and share your valuable feedback with us.