RESTful API testing is one of the most important parts of software quality assurance. Compared to UI testing, API testing is more technical and requires an additional understanding from you of how an application works inside. In addition to that, in API tests you need to deal with JSON and XML which are used for data representation.

That’s why during this type of testing, we need to care about the technical side, but we should still think about our tests from the point of view of a real application user. This will help us understand the app better, keep tests clean and maintainable, maintain a clear idea of specifications coverage and will enable the sharing of newly created tests across your team.

There are many wonderful open source tools available for each programming language that can achieve all the mentioned goals. Today we are going to check out Serenity and REST Assured.

What is Serenity BDD?

Serenity is an open source test automation framework (also known as Thucydides) that helps you write well-structured functional tests, and provides outstanding test reports. The main advantage of Serenity reporting is that it not only provides raw test results, but also a detailed specification of the tested feature. The Serenity framework is used by many organizations due to a wide variety of features which are available out of the box.

Here is a short list of its main features:

Out-of-the-box integration with the most popular test frameworks (JUnit, Cucumber, Rest-Assured, Selenium and many others)

Many built-in features (parallel execution, screenshots for UI tests, Data Driven testing)

Detailed reports

Integration with any build tool (maven, gradle, ant)

What is Rest Assured?

Rest Assured is one of the most powerful libraries for testing RESTful API using Java language. This should be the first-choice when you need to test a REST. All tests should be written in the BDD (Behavior Driven Development) format and its framework syntax is very clean and easy to use. The framework sends a network request to an application under test and verifies the response based on the expectations.

The main features:

Specification reuse

Response validation

XPath verification

Behavior-driven development syntax

In our example, the Serenity framework will be used as a wrapper around the REST Assured tests. We will learn how to setup a project structure to use Serenity’s important features. The main thing we will try to cover is how you can avoid code duplication by creating reusable methods and specifications.

Project Setup

Let’s start with the project creation. All you need in the first step is to create a Maven project with a basic structure in your favorite development environment (as for me, I prefer IntelliJ IDEA):

After that we can start adding the required Maven dependencies to the pom.xml file. We can get these dependencies from the Maven central repository:

Also, in order to run Serenity tests with Maven, you need to add several Maven plugins to the pom.xml file:

If you are familiar with Maven, you can compose the correct pom.xml file yourself. But we can save you time and just give an example of pom.xml which you should get at the end (just replace your current pom.xml if you are following our steps):

<project xmlns= "http://maven.apache.org/POM/4.0.0" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion> 4.0 . 0 </modelVersion> <groupId>com. blazemeter </groupId> <artifactId>serenity-rest-assured-automation</artifactId> <version> 0.0 . 1 -SNAPSHOT</version> <properties> <serenity. version > 1.5 . 8 </serenity. version > <serenity. maven . version > 1.5 . 8 </serenity. maven . version > <junit. version > 4.12 </junit. version > <slf4j. version > 1.6 . 1 </slf4j. version > <maven. failsafe . plugin . version > 2.18 </maven. failsafe . plugin . version > <maven. compiler . plugin . version > 3.2 </maven. compiler . plugin . version > <java. version > 1.8 </java. version > </properties> <dependencies> <dependency> <groupId>net. serenity -bdd</groupId> <artifactId>serenity-core</artifactId> <version>${serenity. version }</version> </dependency> <dependency> <groupId>net. serenity -bdd</groupId> <artifactId>serenity-junit</artifactId> <version>${serenity. version }</version> </dependency> <dependency> <groupId>net. serenity -bdd</groupId> <artifactId>serenity-rest-assured</artifactId> <version>${serenity. version }</version> </dependency> <dependency> <groupId>org. slf4j </groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j. version }</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit. version }</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>${maven. failsafe . plugin . version }</version> <configuration> <includes> <include>**/*. java </include> </includes> </configuration> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org. apache . maven . plugins </groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${java. version }</source> <target>${java. version }</target> </configuration> </plugin> <plugin> <groupId>net. serenity -bdd. maven . plugins </groupId> <artifactId>serenity-maven-plugin</artifactId> <version>${serenity. maven . version }</version> <dependencies> <dependency> <groupId>net. serenity -bdd</groupId> <artifactId>serenity-core</artifactId> <version>${serenity. version }</version> </dependency> </dependencies> <executions> <execution> <id>serenity-reports</id> <phase>post-integration-test</phase> <goals> <goal>aggregate</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>

Now we are ready to start writing our first Rest Assured test.

Your First REST Assured test

To show Serenity and Rest Assured integration in action we need to use a publicly available web site which provides REST APIs. We can use the free GroupKT web site, which provides different public RESTful web services that allow you to perform such operations like country search by alpha numeric ISO codes.

Let’s try to write our first test. To illustrate the approach of writing tests using REST Assured, here is an example of the test that checks that by using the web service “/country/get/iso2code/” API we can find the “United States of America” country with the “US” ISO code:

package tests; import io.restassured.RestAssured; import org.junit.Test; import static org. hamcrest . Matchers . is ; public class CountriesSearchTests { @Test public void verifyThatWeCanFindUnitedStatesOfAmericaUsingTheCodeUS (){ RestAssured. when (). get ( "http://services.groupkt.com/country/get/iso2code/US" ). then(). assertThat (). statusCode ( 200 ). and(). body ( "RestResponse.result.name" , is( "United States of America" )); } }

You can see that the REST Assured syntax is very natural and human readable. In addition, it is obvious that it follows the BDD principle by providing the well-known “Given,When,Then” syntax that makes tests easy to read and maintain.

REST Assured also provides powerful response validation capabilities. In the mentioned example we verify the response code and the response body. But we can also verify the response latency, size, headers, encoding and actually everything that a response might contain inside.

Before you continue, let’s try to run our tests by using the simple maven command:

mvn clean verify

If the configuration and place and everything is right, you should see a similar output:

-------------------------------------------------------

T E S T S

-------------------------------------------------------

Running tests.CountriesSearchTests

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.101 sec - in tests.CountriesSearchTests

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

The output tells you briefly about the test status and the time spent on running the test suite. Basically, that’s it. All we have done is to create a simple REST test using REST Assured and JUnit frameworks. We haven’t used any Serenity components, but don’t worry - outstanding Serenity reporting (mentioned above) is on our path. Now it’s time to continue with writing the tests the way it’s supposed to be done in the Serenity framework.

Organizing Your Tests in Serenity

To ensure that our test works not only for one specific country, let’s add two more tests for two different countries.

package tests; import io.restassured.RestAssured; import org.junit.Test; import static org. hamcrest . Matchers . is ; public class CountriesSearchTests { @Test public void verifyThatWeCanFindUnitedStatesOfAmericaCountryUsingTheCodeUS (){ RestAssured. when (). get ( "http://services.groupkt.com/country/get/iso2code/US" ). then(). assertThat (). statusCode ( 200 ). and(). body ( "RestResponse.result.name" , is( "United States of America" )); } @Test public void verifyThatWeCanFindIndiaCountryUsingTheCodeIN (){ RestAssured. when (). get ( "http://services.groupkt.com/country/get/iso2code/IN" ). then(). assertThat (). statusCode ( 200 ). and(). body ( "RestResponse.result.name" , is( "India" )); } @Test public void verifyThatWeCanFindBrazilCountryUsingTheCodeBR (){ RestAssured. when (). get ( "http://services.groupkt.com/country/get/iso2code/BR" ). then(). assertThat (). statusCode ( 200 ). and(). body ( "RestResponse.result.name" , is( "Brazil" )); } }

On the one hand, you can see the tests look more or less clean and easy to read. But on the other hand, they feel a bit overcrowded with plenty of code duplication. Of course, we can refactor the class itself and create a reusable function that will be placed in the same class as the tests. But what if we need to use the same check somewhere else? We can’t predict that we won’t have any other tests anymore in the future in our test project that should verify country search by ISO codes.

Therefore, it is obvious that it is better to keep this function separately. In case of any response changes, we have to fix the logic in all tests. In our example, it’s not so difficult. But just imagine that you have a few hundred tests. The solution is to implement reusable methods and use these methods in all the tests. Let’s refactor our code to implement some of these good practices.

In Serenity, all tests are split into reusable blocks called “steps”. The main principle of the BDD approach is that we are trying to keep complexity to a high level human readable level. (This allows us to operate with tests and objects not using technical terms like “response code is 200” or “execute request”, but with human readable sentences like “action was successful” or “perform search”). Therefore, it’s always better to start from the smallest steps. When they are implemented you can later use these bricks in more complicated steps. So the main principle to make your tests maintainable is DRY (Don’t Repeat Yourself).

First of all, let’s create a separate package to keep our steps. It is always better to keep them separate as it shows which classes contain reusable components.

As we discussed before, it is better to make steps smaller. So let’s make separate reusable steps from our tests:

package steps; import io.restassured.response.Response; import net.serenitybdd.rest.SerenityRest; import net.thucydides.core.annotations.Step; import static org. hamcrest . Matchers . is ; public class CountriesSearchSteps { private String ISO_CODE_SEARCH = "http://services.groupkt.com/country/get/iso2code/" ; private Response response; @Step public void searchCountryByCode (String code){ response = SerenityRest. when (). get (ISO_CODE_SEARCH + code); } @Step public void searchIsExecutedSuccesfully (){ response. then (). statusCode ( 200 ); } @Step public void iShouldFindCountry (String country){ response. then (). body ( "RestResponse.result.name" , is(country)); } }

If you want, you can also customize steps description that are presented in Serenity reports. For that, you can add a string argument value to override the default step name. Also, if you put references in the string like “{0}, {1}...”, they will be replaced by method parameters:

@Step(“I try to search the country by { 0 } code”) public void searchCountryByCode (String code) {

Note that all steps have “void” as the return type. This helps keep the tests clean and to perform easy integrations with BDD frameworks like JBehave or JVM Cucumber. If you want to pass data between steps, the ‘response’ member variable can be shared between all test steps in that class.

Now our steps are ready. Let’s refactor the main class with our tests:

package tests; import net.serenitybdd.junit.runners.SerenityRunner; import net.thucydides.core.annotations.Steps; import org.junit.Test; import org.junit.runner.RunWith; import steps.CountriesSearchSteps; @RunWith(SerenityRunner. class ) public class CountriesSearchTests { @Steps CountriesSearchSteps countriesSearchSteps; @Test public void verifyThatWeCanFindUnitedStatesOfAmericaCountryUsingTheCodeUS () { countriesSearchSteps. searchCountryByCode ( "US" ); countriesSearchSteps. searchIsExecutedSuccesfully (); countriesSearchSteps. iShouldFindCountry ( "United States of America" ); } @Test public void verifyThatWeCanFindIndiaCountryUsingTheCodeIN (){ countriesSearchSteps. searchCountryByCode ( "IN" ); countriesSearchSteps. searchIsExecutedSuccesfully (); countriesSearchSteps. iShouldFindCountry ( "India" ); } @Test public void verifyThatWeCanFindBrazilCountryUsingTheCodeBR (){ countriesSearchSteps. searchCountryByCode ( "BR" ); countriesSearchSteps. searchIsExecutedSuccesfully (); countriesSearchSteps. iShouldFindCountry ( "Brazil" ); } }

Now our tests definitely look more readable. If our response needs to be changed after a certain time, we don’t need to change the response validation in each test, but only in the “iShouldFindCountry” step definition in the “CountriesSearchSteps” class. Please, also note that we do not need to initialize the steps class as all annotated steps classes are automatically initialized by Serenity.

Serenity Tests Execution and Reporting

One more important thing we added is the “@RunWith(SerenityRunner.class)" annotation on top of the class. As we have now organized our structure to meet some basic Serenity principles, we are ready to run the test using Serenity. This time (after we added the mentioned annotation) these tests will be run using the “SerenityRunner”. For that we can use exactly the same command to run our tests:

mvn clean verify

In the console you should find printed messages for tests start. At the same time under target directory you can find the HTML generated report we were talking about before:

You can open the report in any browser:

If you click on any test you should see a detailed description of the test steps:

One of the most important features of the Serenity and REST Assured integration is that by using detailed reporting, you can easily validate all requests and response details even if you are not adding any logs inside tests. Like the example above, for each executed REST request you can click the button “REST Query” and get a detailed request and response description:

Usually, if you run RESTful API tests without proper steps logging, you might miss the reason of a failed API call. To get a clear picture, you have to run the failed test in debug mode to verify what is going on step by step. But by using powerful Serenity reporting you have a detailed explanation of all the executed steps. So you just need to perform some trivial step analyses and almost always the reason becomes clear without any additional tests debugging.

As you can see, Serenity and REST Assured provide you with a wonderful combination. REST Assured keeps API testing clean and easy to maintain, while Serenity gives you outstanding test reporting and flexibility in running and grouping your tests inside a test suite. Using these tools, tests creation process become to be easy and efficient.

Looking to automate your API tests? Get your API Testing started with BlazeMeter.

Click here to subscribe to our newsletter.