How to write automated tests as a QA
There are innumerable articles on how to write automated tests. Well, here's one more - they are countless right so no one's counting? In this article, I'll share how I write automated tests. Guided by the test pyramid, we will look at API tests and in a later article, UI tests.
If you aren't familiar with the test pyramid, it's a framework that highlights how tests should be concentrated in a software development project. Follow this link for a deep dive on the subject.
Following on from my previous article, we will continue with the registration and login flow. Picture this: the back-end team has exposed the long-awaited APIs for the front-end to consume. Now it's up to QA to ensure this bridge is sound and remains sturdy for aeons to come (well until there is an update to the API spec. really). We will act as the guardrails that validate this integration.
The starting point is to ensure the API documentation is available and user-friendly (who wants to consume something they cannot understand). With documentation dusted, we can pick up the tools of the trade.
My goto tool for API integration is RestAssured, more details here. Depending on how the APIs have been exposed, you have the option to send requests to a given endpoint based on the API contract or build out the tests right in the repo. We will explore the former and send requests to a given endpoint based on the API contract (just like you'd do via Postman or SoapUI).
Now, before commencing the work to write the automated tests, some pertinent questions need answering:
why are we writing automated tests?
what are we automating?
who is writing these tests?
The why part of any endeavour is always critical. Some of the answers to this question could be, to speed up our manual efforts, and to help focus on more complex parts of the system that cannot be easily automated. Getting to the why will make the process 10X easier. It'll automatically feed into what are we automating. This could be complete use cases/features (though some may regard this as an anti-pattern). As to who is writing the tests, the title says it all. However, QA can collaborate with developers for greater test coverage and high-quality scripts.
Now for the fun part. Using the registration flow, we will look at a couple of scenarios we can automate and how to do so using RestAssured. When handling user registration, a common requirement usually states that:
- As a user, I want to provide my details (name, phone number and PIN) so that I register. The phone number must be unique
Our Test Scenarios
Using the requirement above, we come up with the scenarios below:
Scenario 1 | Steps | Expected results |
The user supplies all the required information | send a POST request to the /user/register endpoint with the user payload | - The user is registered and the correct response code (201 created) is returned. The response body has details of the created user |
Scenario 2 | Steps | Expected results |
The user does not supply all the required information e.g. phone number | send a POST request to the /user/register endpoint with missing data in the request body | - The user is not registered and the correct error response (400 bad request) |
Scenario 3 | Steps | Expected results |
The user registers with an existing phone number | send a POST request to the /user/register endpoint with an existing phone number in the request body | - The user is not registered and the correct error response (409 Conflict) is returned for an existing number |
Feel free to add more scenarios as you see fit. Do not forget security.
Setting Up the Testing Environment
With the scenarios in tow, we can fire up our IDE and get cracking with the automation. The examples use Java and Gradle is the build management tool. You'll need the prerequisites below to set up the automation.
setup a Gradle project, here's a link on how to do so
add the dependencies
TestNG: this is our test framework and will handle how we setup and run the tests
Hamcrest: this is a handy tool for making assertions. When the test is run, the output/outcome has to meet an expected outcome/criteria. This is where Hamcrest shines and allows tests to be a lot more readable
Allure: the purpose of a test is to give feedback to stakeholders on the state of the application. Allure as a reporting tool handles this well
RestAssured: this simplifies the testing of REST services and follows the given, when, then format to send requests to an endpoint and get the response.
Now we can start looking at how the code is structured. The goal is to ensure our tests are modular, readable and easy to maintain. Part of that entails adhering to the Single Responsibility Principle. How I understand it is, that a class should do one thing and should have only one reason to change. This principle will help as the suite grows and ensures it is flexible and responsive to changes in the requirements.
With that in mind, we see that we need to ensure that our tests handle a single use case. Essentially, the test class should focus on the test e.g. RegisterUserTest
class. We need to introduce other classes that support this test class i.e. classes responsible for sending the request to the backend and returning the response e.g. RegisterUserEndpoint
class. For the request body, a separate class will represent the payload e.g. RegisterUserPayload
class. Here's how this would look:
tests
RegisterUserTest
RegisterInvalidUserTest
endpoints
- RegisterUserEndpoint
model/payload
- UserRegistrationPayload
For those familiar with Postman and sending requests via its interface, you can think of the test class as the steps you take when testing an API. This usually consists of setting up the data required, hitting the send request
button and checking the response to verify it has the expected outcome and status. In essence, the test class mimics these actions; sets up the data, sends the request and asserts the outcome.
The endpoint class is us specifying the URL, configuring the headers, content types, authorisation and request methods (POST, GET, PATCH etc.). Lastly, the payload represents the request body that is part of the request (if it is required).
Writing the Automated Tests
Now we can write our user registration test:
public class RegisterUserTest{
@Test (description = "Register user successfully test")
public void registerUserTest(){
// given, arrange the data
UserRegistrationPayload newUser = UserRegistrationPayload.builder.
name("Test").
phoneNumber("+263770123456").
password("1234").
build();
// when, we act on the data
Response registerUserResponse = RegisterUserEndpoint.registerUser(newUser);
// then, assert the outcome of the action
registerUserResponse.then().
assertThat().
// we check the status code from the response is a 201
statusCode(HttpStatus.SC_CREATED).
// we also expect the response to contain the first name
body("name", is( equalTo( newUser.getName() ))).
// the response should also have the phone number
and().body("phoneNumber", is( equalTo( newUser.getPhonenumber() )));
}
}
The test is relatively clear. We create a user, send a request via the RegisterUserEndpoint and validate the response from the request based on the expected outcomes. Using the same approach, other test classes can be created for the other scenarios we've got. Now let's see what the endpoint class would look like.
public class RegisterUserEndpoint{
private static String registerUserUri = "https://mytestapi.com";
private static String registerPath = "/users/register/";
// the endpoint to register a user
public static Response registerUser(RegisterUserPayload user){
// setup RestAssured to point to your API endpoints
RestAssured.baseURI = registerUserUri;
RestAssured.basePath = registerPath;
Response registerUserResponse = given().
contentType(ContentType.JSON).
body(user).
log.all().
when().
post(); // send POST request to the endpoint
// you can log the request response to see the response in the console
logger.info("User registration response: " + registerUserResponse.asPrettyString());
return registerUserResponse;
}
}
Good stuff. Our endpoint class takes care of setting up RestAssured with the correct endpoints to use. The method registerUser(RegisterUserPayload user)
takes the payload of our request, configures RestAssured with the correct ContentType, adds the request body using the payload we pass in and makes a POST to https://mytestapi.com/users/register/
. The response from the server is what we return to our test and is used to assert whether we got a success or a failure.
Last but not least, we can write up the payload class. This follows the same request body parameters our service expects. We required that the user supplies:
name
phone number
PIN
@Getter @Setter
@Builder
public class RegiserUserPayload{
private String name, password, phoneNumber;
}
The @Getter@Setter
annotations are from Lombok and it's handy as it reduces boilerplate code. That's it, folks.
Voilà, we've seen the steps one can take to automate an API using RestAssured. I prefer this approach where you start with the test. It'll fail and have lots of red wiggly lines. This forces you to focus on what the test should do. We have data (given), that we act on (when) and draw conclusions from (then).
Let me know what you think and happy testing. Next, we take a look at the UI, stay tuned.
Subscribe to my newsletter
Read articles from Tinashe directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by