Testowanie REST API z Serenity i Rest-assured
Rest-assured to framework w Javie służący do testowania i walidacji REST API.
Serenity to framework do automatycznego testowania akceptacyjnego oparty na BDD (Behavior Driven Development). Połączenie Rest-assured z Serenity umożliwia łatwe tworzenie testów API w stylu BDD oraz generowanie estetycznych i przejrzystych raportów.
Projekt testowy
W ramach tego artykułu testowane będzie API postcodes.io.
Postcodes to darmowe, otwarte API dla kodów pocztowych i geolokalizacji w Wielkiej Brytanii.
Struktura projektu
Projekt używa standardowej struktury Maven.
postcodes-api-tests
src
pom.xml
test
java
pl.jlabs
cukes
RunCukes.java
stepdefs
BaseTest.java
NearestPostcodesSteps.java
RandomPostcodeSteps.java
ValidatePostcodeSteps.java
PostcodesEndpoints.java
resources
features
NearestPostcode.feature
RandomPostcode.feature
ValidatePostcode.feature
pom.xml
<!--?xml version="1.0" encoding="UTF-8"?-->
<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>pl.jlabs</groupid>
<artifactid>restassured-serenity</artifactid>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<restassured.version>3.3.0</restassured.version>
<hamcrest.version>1.3</hamcrest.version>
<json.version>20180813</json.version>
<serenity.version>2.0.81</serenity.version>
<serenity.maven.version>2.0.81</serenity.maven.version>
<serenity.cucumber.version>1.0.21</serenity.cucumber.version>
<cucumber.version>4.2.0</cucumber.version>
<encoding>UTF-8</encoding>
<parallel.tests>4</parallel.tests>
</properties>
<dependencies>
<dependency>
<groupid>net.serenity-bdd</groupid>
<artifactid>serenity-core</artifactid>
<version>${serenity.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupid>io.cucumber</groupid>
<artifactid>cucumber-core</artifactid>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupid>io.cucumber</groupid>
<artifactid>cucumber-java</artifactid>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupid>io.cucumber</groupid>
<artifactid>cucumber-junit</artifactid>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupid>net.serenity-bdd</groupid>
<artifactid>serenity-junit</artifactid>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>net.serenity-bdd</groupid>
<artifactid>serenity-rest-assured</artifactid>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>net.serenity-bdd</groupid>
<artifactid>serenity-cucumber4</artifactid>
<version>${serenity.cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>junit</groupid>
<artifactid>junit</artifactid>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>org.assertj</groupid>
<artifactid>assertj-core</artifactid>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>org.hamcrest</groupid>
<artifactid>hamcrest-all</artifactid>
<version>1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-surefire-plugin</artifactid>
<version>2.22.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactid>maven-failsafe-plugin</artifactid>
<version>2.22.1</version>
<configuration>
<includes>
<include>**/*Cukes.java</include>
</includes>
<parallel>classes</parallel>
<threadcount>${parallel.tests}</threadcount>
<forkcount>${parallel.tests}</forkcount>
</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>
<version>3.8.0</version>
<configuration>
<source>1.8
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupid>net.serenity-bdd.maven.plugins</groupid>
<artifactid>serenity-maven-plugin</artifactid>
<version>${serenity.maven.version}</version>
<executions>
<execution>
<id>serenity-reports</id>
<phase>post-integration-test</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Serenity jest kompatybilne z Cucumber w wersjach 2.x i 4.x. Aby używać Cucumber w wersji 4.x, należy wykluczyć cucumber-core z serenity-core.
Pluginy Maven failsafe oraz serenity-maven-plugin muszą być odpowiednio skonfigurowane, aby generować raporty testów.
PostcodesEndpoint.java
Został stworzony Enum z adresami URL dla endpointów.
package pl.jlabs;
public enum PostcodesEndpoints {
NEAREST("postcodes?lon={lon}&lat={lat}"),
RANDOM("/random/postcodes"),
VALIDATE("postcodes/{postcode}/validate");
private final String url;
PostcodesEndpoints(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
}
RunCukes.java
Runner testów Cucumber.
package pl.jlabs.cukes;
import cucumber.api.CucumberOptions;
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.runner.RunWith;
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(
plugin = {"pretty"},
features = "classpath:features",
glue = {"pl.jlabs.stepdefs"}
)
public class RunCukes {}
NearestPostcodesSteps.java
package pl.jlabs.stepdefs;
import PostcodesEndpoints;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import net.serenitybdd.rest.SerenityRest;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.http.HttpStatus;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.serenitybdd.rest.SerenityRest.restAssuredThat;
import static org.hamcrest.Matchers.*;
public class NearestPostcodesSteps extends BaseTest {
private static final Map<string,object> VALID_GPS_DATA = Stream.of(
new AbstractMap.SimpleEntry<>("lon", "0.629834723775309"),
new AbstractMap.SimpleEntry<>("lat", "51.7923246977375"),
new AbstractMap.SimpleEntry<>("postCodes", new ArrayList<>(Arrays.asList("CM8 1EF", "CM8 1EU", "CM8 1PH", "CM8 1PQ"))))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
private static final List<string> COORDINATES_OUTSIDE_UK = new ArrayList<>(Arrays.asList("50.049683", "19.944544"));
@Given("the request with valid coordinates to the Nearest Postcodes endpoint")
public void theRequestWithAValidCoordinatesToTheNearestPostcodesEndpoint() {
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("lon", VALID_GPS_DATA.get("lon"))
.pathParam("lat", VALID_GPS_DATA.get("lat"));
}
@Given("the request with invalid longitude")
public void theRequestWithInvalidLongitude() {
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("lon", RandomStringUtils.randomAlphabetic(3))
.pathParam("lat", VALID_GPS_DATA.get("lat"));
}
@Given("the request with valid coordinates outside UK")
public void theRequestWithAValidCoordinatesOutsideUK() {
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("lon", COORDINATES_OUTSIDE_UK.get(0))
.pathParam("lat", COORDINATES_OUTSIDE_UK.get(1));
}
@Given("the request with invalid latitude")
public void theRequestWithInvalidLatitude() {
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("lon", VALID_GPS_DATA.get("lon"))
.pathParam("lat", RandomStringUtils.randomAlphabetic(3));
}
@When("the request is made with the get method to nearest postcodes endpoint")
public void theRequestIsMadeWithGetMethod() {
SerenityRest.when().get(PostcodesEndpoints.NEAREST.getUrl());
}
@Then("the response with a correct list of postcodes is returned")
public void theResponseWithACorrectListOfPostcodesIsReturned() {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_OK)
.body("result", hasSize(((List) VALID_GPS_DATA.get("postCodes")).size()))
.body("result[0].postcode", equalTo(((List) VALID_GPS_DATA.get("postCodes")).get(0)))
.body("result[1].postcode", equalTo(((List) VALID_GPS_DATA.get("postCodes")).get(1)))
.body("result[2].postcode", equalTo(((List) VALID_GPS_DATA.get("postCodes")).get(2)))
.body("result[3].postcode", equalTo(((List) VALID_GPS_DATA.get("postCodes")).get(3))));
}
@Then("an empty result list is returned")
public void anEmptyResultListIsReturned() {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_OK)
.body("result", is(nullValue())));
}
@Then("the invalid longitude latitude submitted error is returned")
public void theInvalidLongitudeLatitudeSubmittedErrorIsReturned() {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_BAD_REQUEST)
.body("status", equalTo(HttpStatus.SC_BAD_REQUEST))
.body("error", equalTo("Invalid longitude/latitude submitted")));
}
}
Serenity udostępnia klasę SerenityRest do korzystania z Rest-assured.
Dla zastosowania BDD (Behavior Driven Development), SerenityRest jest używana dwukrotnie. W kroku Given:
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("lon", VALID_GPS_DATA.get("lon"))
.pathParam("lat", VALID_GPS_DATA.get("lat"));
oraz w kroku When, gdzie wykonuje się wywołanie do API:
SerenityRest.when().get(PostcodesEndpoints.NEAREST.getUrl());
W celu przeanalizowania odpowiedzi wywoływana jest metoda restAssuredThat z SerenityRest:
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_BAD_REQUEST)
.body("status", equalTo(HttpStatus.SC_BAD_REQUEST))
.body("error", equalTo("Invalid longitude/latitude submitted")));
Można dokonać również asercji przy użyciu obiektu SerenityRest:
SerenityRest.then()
.statusCode(HttpStatus.SC_OK)
.body("result", is(notNullValue()));
RandomPostcodeSteps.java
package pl.jlabs.stepdefs;
import PostcodesEndpoints;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import net.serenitybdd.rest.SerenityRest;
import org.apache.http.HttpStatus;
import static net.serenitybdd.rest.SerenityRest.restAssuredThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
public class RandomPostcodeSteps extends BaseTest {
@Given("a request to the random postcodes endpoint")
public void aRequestToTheRandomPostcodesEndpoint() {
SerenityRest.given()
.baseUri(BASE_URI);
}
@When("the request is made with the get method to random postcode")
public void theRequestIsMadeWithTheGetMethodToRandomPostcode() {
SerenityRest.when().get(PostcodesEndpoints.RANDOM.getUrl());
}
@Then("the randomly generated postcode is returned")
public void theRandomlyGeneratedPostcodeIsReturned() {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_OK)
.body("result", is(notNullValue())));
}
}
ValidatePostcodeSteps.java
package pl.jlabs.stepdefs;
import PostcodesEndpoints;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import net.serenitybdd.rest.SerenityRest;
import org.apache.http.HttpStatus;
import static net.serenitybdd.rest.SerenityRest.restAssuredThat;
import static org.hamcrest.Matchers.equalTo;
public class ValidatePostcodeSteps extends BaseTest {
@Given("the request with a valid postcode")
public void theRequestWithAValid() {
validatePostCodeRequest(VALID_POST_CODES_LIST.get(0));
}
@Given("the request with an invalid postcode")
public void theRequestWithAnInvalidPostcode() {
validatePostCodeRequest(getRandomInvalidPostCode());
}
@Given("the request without postcode")
public void theRequestWithoutPostcode() {
validatePostCodeRequest("");
}
@When("the request is made with the get method to validate postcode")
public void theRequestIsMadeWithTheGetMethodToValidatePostcode() {
SerenityRest.when().get(PostcodesEndpoints.VALIDATE.getUrl());
}
@Then("the response with a result equal to {string} is returned")
public void theResponseWithAResultEqualToIsReturned(String expectedResult) {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_OK)
.body("result", equalTo(Boolean.valueOf(expectedResult))));
}
@Then("the not found status code with an Invalid postcode error is returned")
public void theNotFoundStatusCodeWithInvalidPostcodeErrorIsReturned() {
restAssuredThat(response -> response
.statusCode(HttpStatus.SC_NOT_FOUND)
.body("error", equalTo("Invalid postcode")));
}
private void validatePostCodeRequest(String postCode) {
SerenityRest.given()
.baseUri(BASE_URI)
.pathParam("postcode", postCode);
}
}
NearestPostcode.feature
Plik Feature z scenariuszami dla endpointu najbliższych kodów pocztowych:
Feature: Nearest postcode
Scenario: A correct list of postcodes is returned for valid coordinates
Given the request with valid coordinates to the Nearest Postcodes endpoint
When the request is made with the get method to nearest postcodes endpoint
Then the response with a correct list of postcodes is returned
Scenario: An empty results list is returned for a valid coordinates outside UK
Given the request with valid coordinates outside UK
When the request is made with the get method to nearest postcodes endpoint
Then an empty result list is returned
Scenario: An invalid longitude/latitude submitted error is returned for a request with invalid longitude
Given the request with invalid longitude
When the request is made with the get method to nearest postcodes endpoint
Then the invalid longitude latitude submitted error is returned
Scenario: An invalid longitude/latitude submitted error is returned for a request with invalid latitude
Given the request with invalid latitude
When the request is made with the get method to nearest postcodes endpoint
Then the invalid longitude latitude submitted error is returned
RandomPostcode.feature
Plik Feature z scenariuszami dla generowania losowego kodu pocztowego:
Feature: Random postcode
Scenario: A random postcode is generated
Given a request to the random postcodes endpoint
When the request is made with the get method to random postcode
Then the randomly generated postcode is returned
ValidatePostcode.feature
Plik Feature z scenariuszami do walidacji kodu pocztowego:
Feature: Validate postcode
Scenario: True is returned for a request with a valid postcode
Given the request with a valid postcode
When the request is made with the get method to validate postcode
Then the response with a result equal to "true" is returned
Scenario: False is returned for a request with an invalid postcode
Given the request with an invalid postcode
When the request is made with the get method to validate postcode
Then the response with a result equal to "false" is returned
Scenario: Not found with an invalid postcode error is returned for a request without postcode
Given the request without postcode
When the request is made with get method to validate postcode
Then the not found status code with an Invalid postcode error is returned
Wykonanie testów
Aby uruchomić testy, użyj polecenia Maven:
mvn clean verify
Raport z testów
Po wykonaniu testów, w katalogu target/site/serenity zostanie wygenerowany raport. Aby go wyświetlić, otwórz plik index.html.
Raport wyświetlany jest w przyjaznej formie dla użytkownika.
W zakładce „Test Results” dostępne są szczegółowe raporty scenariuszy dla każdego przypadku. Poniżej raport dla scenariusza „True is returned for a request with a valid postcode”:
Przy użyciu przycisku „REST Query” można wyświetlić szczegóły zapytania. Widoczne są następujące szczegóły:
- Ścieżka (Path)
- Kod statusu (Status code)
- Nagłówki zapytania (Request Headers)
- Treść zapytania (Request Body)
- Ciasteczka zapytania (Request Cookies)
- Nagłówki odpowiedzi (Response Headers)
- Treść odpowiedzi (Response Body)
Podsumowanie
Serenity w połączeniu z Rest-assured tworzy idealny zestaw do tworzenia testów REST API w podejściu BDD. Implementacja testów jest prawie taka sama, jak w przypadku użycia samego Rest-assured, z niewielkim dodatkiem konieczności użycia obiektu SerenityRest jako „prefiksu”.
Ogromną zaletą Serenity są raporty, które zawierają pełne informacje o zapytaniach i odpowiedziach. Dodatkowo są przedstawione w przyjazny sposób co ma wpływ na szybkość ich analizowania.