Beginners Guide to REST API Testing
This article will detail how to write REST API Test Automation in Java. First, let's explain the term. REST is an acronym for REpresentational State Transfer and is a set of rules and conventions that allow different software applications to communicate and exchange data over the Internet. REST provides interaction between the user (for example, web browser or mobile app) and the server (where data is stored) in a standardized way.
For this article, we will use RESTful API.
You can find the complete documentation on the page above. You can also try to copy the request CURL and import it to the Postman application. This application helps execute REST API development requests in Java. This would help you understand which requests we have executed during automation testing.
The mentioned Java API supports five common types of requests:
- GET helps you get information about one or more elements. We also have the GetBookingIds request, which produces filtered booking IDs. Another example is GetBooking, which returns a booking with the specified ID.
- POST creates a new element. In the example API, we have a CreateBooking request, which creates a new booking.
- PUT updates an element. One example is an UpdateBooking request. It updates a specified booking, but we must add all booking attributes in the body.
- PATCH updates a part of the element. In the example API, a PartialUpdateBooking request allows us to update only part of the booking's attribute. We are mentioning only attributes that we want to change.
- DELETE allows us to delete the element. A DeleteBooking request deletes a booking with a specified ID.
It is crucial to ensure the correct proportion of test types when testing. Automated API tests are faster and more stable than automated GUI tests. That's why REST API testing should be more than GUI tests.
We should follow the testing pyramid to avoid the ice cream cone antipattern. UI tests can generate test data before running UI tests.
Popular libraries for testing
Let's focus on libraries that we would need for our tests. I would also add libraries, which wouldn't be necessary, but they would help us.
Let's start with JUnit. It is an open-source framework for unit tests in Java.
Firstly, we must use the Rest Assured Library to execute API requests and check their returns. This library will also enable us to make assertions and have more readable assertions, such as fluent assertions. For more complex assertions involving arrays, custom objects, and more, we can use AssertJ. Additionally, we will utilize annotations from JUnit 5 in this article, as there are different annotations between JUnit 4 and JUnit 5.
During API tests, you must serialize POJO (Plain Old Java Object) into JSON and vice versa (deserialize). Jackson API would help a lot in this case. We would also use Lombok not to write every getter and setter.
Another case is test data. You can use libraries to generate test addresses, names, and more. In this article, we will use JavaFaker.
Also, you can create mocks and stubs to simulate answers for some requests. Popular mocking frameworks are Mockito and EasyMock. Using mocking helps your REST API testing behavior without depending on the external REST services, making the tests more reliable and repeatable.
Helpful JUnit annotations
There are helpful annotations in JUnit that would help you organize tests. We will shortly describe part of them. Keep in mind that we are not referring to all of them. Let's be selective in our discussion.
@Test
Thanks to this annotation, a method is recognized as a test. Remember that this method shouldn't be private. Also, it is recommended to be void because returned values would be ignored.
@BeforeEach @AfterEach
Those annotations before methods would run part of the code before or after every test. This could be used to set up test data updated during tests. In JUnit 4, we had @Before and @After.
@BeforeAll @AfterAll
Thanks to the mentioned annotations, you would run a method only once before or after all tests in the class. Previously, in JUnit 4, we had @BeforeClass and @AfterClass.
@Nested
This would help you use a class with tests inside the tests class. You have to add this annotation before the nested class
@Disable
You can turn off a test class or a test method. Previously, you had @Ignore annotation. If you want to disable a test under specified conditions, you must use an assumption.
@DisplayName
You can have a custom display test name other than the method name.
@RepeatedTest
Thanks to this annotation, you can repeat tests. Just remember not to use @Test annotation in this situation if you don't want to have an extra run.
Simple requests
Let's start with a simple request. We want to make a Get method to check if the API is responding.
Let's go through the used keywords.
- given() describes the initial condition for our request. In our example, we have a base URI and content type. We can also specify headers, path parameters, and request body.
- when() describes a type of request and used endpoint.
- then() describes how we want to validate a request result. In the example, we check the result code, which is expected to be 200. However, we can also check the whole result, like body, cookies, response headers, etc.
Let's make a post request.
In this example, we generate a token that can be useful in handling post, update, and delete requests. As you can see, the test has a pre-defined body. We extract the token and assign it to the token variable. We then use an assertion from AssertJ to verify that the token is not empty. This token can be used in other requests. However, it is important to note that this is just a simple example. In reality, it is not recommended to store passwords in the code. Instead, they should be stored in a separate file for security reasons.
You can also get a list from the response body. The next test would execute another GET request, returning a list of IDs.
Our response object is a list of elements with the "bookingId" attribute. This is an example answer:
We've extracted the attribute's value in the test and saved it to the list. Thanks to the AssertJ method, we have checked if the list is empty. You can also use an assert method from Hamcrest.
Serializing and deserializing (POJO and JSON object)
Sometimes, we want to convert JSON into a simple Java object POJO.
Making use of Jackson libraries can prove to be advantageous in that particular scenario. The main library is Jackson Core. In our following example, we will use the Jackson Datatype: Joda. This client library would help us to get data variables.
We want to test the GET method, which is returning a booking.
This is an example API result.
We need to create the Booking and the BookingDates classes.
BookingDates:
This is a model for bookingdate. As you can see, we have used an annotation called JsonProperty. It helps us to have a JSON attribute name different from the variable name. Thanks to JsonFormat, we can have a Date variable in POJO.
Booking:
This is a model used for booking entry. In this scenario, there is no need for any annotations. Both classes contain public getters and setters, essential for serialization and deserialization. Although the IDE may suggest that the REST methods are not used, this is incorrect. Getters and setters are critical to the process of serialization and deserialization.
Now, we can execute the request, deserialize the result into a POJO, and implement a request method to retrieve an ID.
We can also serialize POJO into JSON. We need it in a POST request to create a new booking, so we must update the Booking class.
We don't want to create a Booking instant with a constructor with many parameters. That's why we have implemented a design pattern called builder. As you can see, the builder has methods like "withFirstName" to specify a value for attributes. Ultimately, we use the "build" method to create an instance of Booking with the builder's data. We still want to be able to deserialize; that's why we had to add a constructor without a parameter. Also, we want to have some test data, so we have used the JavaFaker library. Let's implement a test and use the updated class.
During the test, we made an instant booking using the default values and modified only some attributes. We then retrieved the booking ID from the response code. You can also verify other aspects, such as ensuring that the values of a created booking match the expected values.
Lombok
As you can see, we had to write many code in the Booking and BookingDates classes. However, using the Lombok library can significantly reduce the number of code lines. Let's take a look at the updated BookingDates class.
Thanks to @Data annotation, we don't have to write getters and setters.
In Booking, we also don't have to write setters and getters. Also, we want to use extra annotation instead of a builder. In this situation, we use @Builder. There are also annotations for constructors, like @NoArgsConstructor and @AllArgsConstructor. We want to have the default data, so @Builder.Default will also be helpful. You can also still use your builder but need other annotations.
Now, look at the updated test with the default builder.
As you can see, we have builder() and can set values with methods with property names.
To have suggestions from IDE, a plugin would be helpful. Also, for the gradle setup, let's look at the Lombok official page.
Authentication
There are many ways of authentication, for example, basic, digest, or oath2. In the example project, we have two possibilities. We can use basic authentication or a token in cookies for some methods.
As you can see, we used preemptive().basic() for primary authentication. We can get a token for the same credentials and add it to the cookies.
We have added tokens using the cookie() method. Also, you can notice that we have used pathParam() to add a variable to patch(). Also, you can notice that we have used a feature in assertJ. We are comparing two objects, but we want to ignore some fields because in exampleData we have only part of the booking's parameters that we wanted to update. Thanks to .usingRecursiveComparison().ignoringFields(), we could achieve it.
Logs
Logs are very easy to turn on. You can add it to each request or set it up globally. There are separate logs for sent requests and received answers. You should add a log method in a "given" or "then" section. You can log everything by adding the .log().all() method, the most popular option. However, you have different other options. You can log only a body, header, or cookies. You can also log in only under specified conditions. You have special methods for it. Here are the mentioned methods:
Assumptions
Sometimes, we want to abort tests under some conditions, for example, a chosen environment. In that situation, assumption would be very helpful. We have three methods: assumeTrue(), assumeFalse(), and assumingThat().
The example shows that we would disable the tests depending on the API status code. In checkAuthToken, we have assumeTrue(). That's why this test would run only if the status would be 200. On the other hand, checkGetBookingIds would be executed only if the status is different from 200. Let's check the results for the status 200.
As you can see, one test passed, and another was aborted.
Now, let's look at the assmingThat() method.
In that case, we would check API status only on the dev environment.
Request builder
As you may have noticed, we have been setting request configuration in every test. That's against the rule DRY (Don't Repeat Yourself). Let's think about how to fix it.
As you may have noticed, we have created an instance of RequestSpecification and ResponseSpecification. We have set the base URI, content type, and logging level in the request specification. In the response specification, we have developed the level of logging, expected status code, and expected content type. The mentioned specifications we have to assign to the request by spec() method in the correct section. Thanks to this, we don't have to set requests from the beginning of every test.
Conclusion
The examples mentioned in the article show numerous features of the libraries. However, it is essential to note that this topic is complex, and the article does not cover all the possibilities and resources available. The examples provided are merely simple ones. In more complex integration test projects, you must consider its architectural style and ensure it adheres to clean code principles. Also, storing credentials in a secure and separate location would be best.
Related Hyperskill topics
like this