REST Assured: большой гайд

Библиотека, разработанная для упрощения тестирования и валидации REST API, с учетом практик, применяемых в динамических языках Ruby и Groovy. 

Первый тест

Сначала убедимся, что выполнены статические импорты:

io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*

Это нужно для упрощения тестов и быстрого доступа к основным API. 

В примере ниже — простая система игровых ставок на футбол:

{
    "id": "390",
    "data": {
        "leagueId": 35,
        "homeTeam": "Norway",
        "visitingTeam": "England",
    },
    "odds": [{
        "price": "1.30",
        "name": "1"
    },
    {
        "price": "5.25",
        "name": "X"
    }]
}

Это JSON-ответ из локально развернутого API: http://localhost:8080/events?id=390 :

Теперь при помощи REST-assured верифицируем некоторые вещи в JSON-ответе:

@Test
public void givenUrl_whenSuccessOnGetsResponseAndJsonHasRequiredKV_thenCorrect() {
   get("/events?id=390").then().statusCode(200).assertThat()
      .body("data.leagueId", equalTo(35)); 
}

Верифицируем, что на вызов к эндпойнту /events?id=390 получено тело, содержащее JSON String, в которой leagueId объекта data равен 35.

Еще пример. Нужно верифицировать, что массив odds включает элементы с ценами между 1,30 и 5,25:

@Test
public void givenUrl_whenJsonResponseHasArrayWithGivenValuesUnderKey_thenCorrect() {
   get("/events?id=390").then().assertThat()
      .body("odds.price", hasItems("1.30", "5.25"));
}

Настройка 

Если используете стандартный инструмент для зависимостей Maven, добавляем следующую зависимость в файл pom.xml:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>3.3.0</version>
    <scope>test</scope>
</dependency>

Последняя версия её — здесь.

REST-assured для своих assertions применяет мощные матчеры Hamcrest, включаем их:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>2.1</version>
</dependency>

Последняя версия здесь.

Валидация Anonymous JSON Root

Представим массив, состоящий из примитивов, а не объектов:

[1, 2, 3]

Это называется anonymous JSON root, что означает, что в нем нет пар ключ:значение, при этом элементы остаются валидными JSON-данными.

Валидация такого сценария выполняется при помощи символа $ или пустой строки (‘’), указанных в качестве пути. Например, если сервис выше в примере доступен по http://localhost:8080/json, валидируем его так:

when().get("/json").then().body("$", hasItems(1, 2, 3));

Или так:

when().get("/json").then().body("", hasItems(1, 2, 3));

Float и Double

В JSON-ответах числа с плавающей точкой преобразуются в примитивный тип float. Применение этого типа не совпадает с применением double как во многих случаях в Java. Например в этом ответе:

{
    "odd": {
        "price": "1.30",
        "ck": 12.2,
        "name": "1"
    }
}

Представим, что выполнили следующий тест значения ck:

get("/odd").then().assertThat().body("odd.ck", equalTo(12.2));

Этот тест упадет, даже если проверяемое значение равно значению, полученному в ответе, потому что мы сравниваем с double, а не с float.

Чтобы все работало, нужно эксплицитно указать операнд в equalTo-матчере как float:

get("/odd").then().assertThat().body("odd.ck", equalTo(12.2f));

Метод запроса

Стандартно мы выполняем запрос, вызывая метод типа get(), соответствующий методу запроса, который хотим использовать. Кроме того можем указывать HTTP-метод, используя метод request():

@Test
public void whenRequestGet_thenOK(){
    when().request("GET", "/users/eugenp").then().statusCode(200);
}

Пример выше эквивалентен применению get().

Таким же образом можем отправлять запросы HEAD, CONNECT и OPTIONS:

<>

POST-запрос имеет такой же синтаксис; можем прописывать его тело с помощью методов with() и body().

Итак, создаем новый Odd, отправляя POST-запрос:

<>

Объект Odd, отправленный как body, автоматически будет конвертирован в JSON. Также можем передать любую String как body в POST.

Конфигурация дефолтных значений

Можем задать дефолтные значения в тестах:

@Before
public void setup() {
    RestAssured.baseURI = "https://api.github.com";
    RestAssured.port = 443;
}

Здесь задан базовый URI и порт для запросов. Кроме того, можем конфигурировать базовый путь, корневой каталог, и аутентификацию.

Примечание: сброс конфигураций REST-assured на изначальные:

RestAssured.reset();

Измерение времени ответа

Далее посмотрим, как измерять время ответа методами time() и timeIn() в объекте Response:

<>

При этом:

  • time() — для получения времени в миллисекундах
  • timeIn() — в указываемых единицах времени

Валидация 

Валидация времени ответа, в миллисекундах, простым long Matcher:

@Test
public void whenValidateResponseTime_thenSuccess() {
    when().get("/users/eugenp").then().time(lessThan(5000L));
}

Если нужно валидировать время в других единицах, используем матчер time() с другим TimeUnit-параметром:

@Test
public void whenValidateResponseTimeInSeconds_thenSuccess(){
    when().get("/users/eugenp").then().time(lessThan(5L),TimeUnit.SECONDS);
}

Верификация XML-ответа

Можно валидировать не только JSON-ответ, но и XML. Например выполнен запрос на http://localhost:8080/employees и получен следующий ответ:

<employees>
    <employee category="skilled">
        <first-name>Jane</first-name>
        <last-name>Daisy</last-name>
        <sex>f</sex>
    </employee>
</employees>

Верифицируем, что first-name равно Jane:

@Test
public void givenUrl_whenXmlResponseValueTestsEqual_thenCorrect() {
    post("/employees").then().assertThat()
      .body("employees.employee.first-name", equalTo("Jane"));
}

Также верифицируем, что все значения соответствуют ожидаемым, выстраивая body-матчеры так:

@Test
public void givenUrl_whenMultipleXmlValuesTestEqual_thenCorrect() {
    post("/employees").then().assertThat()
      .body("employees.employee.first-name", equalTo("Jane"))
        .body("employees.employee.last-name", equalTo("Daisy"))
          .body("employees.employee.sex", equalTo("f"));
}

Или по другому, с аргументами:

@Test
public void givenUrl_whenMultipleXmlValuesTestEqualInShortHand_thenCorrect() {
    post("/employees")
      .then().assertThat().body("employees.employee.first-name", 
        equalTo("Jane"),"employees.employee.last-name", 
          equalTo("Daisy"), "employees.employee.sex", 
            equalTo("f"));
}

Через XPath/XML

Верификация ответов с помощью XPath. В примере ниже — матчер с first-name:

@Test
public void givenUrl_whenValidatesXmlUsingXpath_thenCorrect() {
    post("/employees").then().assertThat().
      body(hasXPath("/employees/employee/first-name", containsString("Ja")));
}

XPath также допускает альтернативный способ с матчером equalTo:

@Test
public void givenUrl_whenValidatesXmlUsingXpath2_thenCorrect() {
    post("/employees").then().assertThat()
      .body(hasXPath("/employees/employee/first-name[text()='Jane']"));
}

Логирование

Деталей запроса

Логирование всех данных из запроса — это log()/all():

@Test
public void whenLogRequest_thenOK() {
    given().log().all()
      .when().get("/users/eugenp")
      .then().statusCode(200);
}

В логе будет такое:

Request method:	GET
Request URI:	https://api.github.com:443/users/eugenp
Proxy:			<none>
Request params:	<none>
Query params:	<none>
Form params:	<none>
Path params:	<none>
Multiparts:		<none>
Headers:		Accept=*/*
Cookies:		<none>
Body:			<none>

Чтобы записать лишь отдельные части запроса, применяем метод log() в сочетании с params(), body(), headers(), cookies(), method(), path(), например: log.().params().

При этом нужно учитывать, что другие библиотеки (или фильтры) могут влиять на то что отправляется на сервер, поэтому это лучше использовать только для логирования первого запроса.

Логирование ответа

Таким же образом логируется и ответ. В примере ниже записываем только тело ответа:

@Test
public void whenLogResponse_thenOK() {
    when().get("/repos/eugenp/tutorials")
      .then().log().body().statusCode(200);
}

Получаем вывод:

{
    "id": 9754983,
    "name": "tutorials",
    "full_name": "eugenp/tutorials",
    "private": false,
    "html_url": "https://github.com/eugenp/tutorials",
    "description": "The \"REST With Spring\" Course: ",
    "fork": false,
    "size": 72371,
    "license": {
        "key": "mit",
        "name": "MIT License",
        "spdx_id": "MIT",
        "url": "https://api.github.com/licenses/mit"
    },
...
}

При условии

Есть опция включать логирование только при ошибке или совпадении статус-кода с указанным:

@Test
public void whenLogResponseIfErrorOccurred_thenSuccess() {
 
    when().get("/users/eugenp")
      .then().log().ifError();
    when().get("/users/eugenp")
      .then().log().ifStatusCodeIsEqualTo(500);
    when().get("/users/eugenp")
      .then().log().ifStatusCodeMatches(greaterThan(200));
}

При неудачной валидации

Логирование запроса и ответа только при неудачной валидации:

@Test
public void whenLogOnlyIfValidationFailed_thenSuccess() {
    when().get("/users/eugenp")
      .then().log().ifValidationFails().statusCode(200);

    given().log().ifValidationFails()
      .when().get("/users/eugenp")
      .then().statusCode(200);
}

— валидация статус-кода 200, если нет, записать в лог.

Заголовки, cookies и параметры

Далее рассмотрим более сложные сценарии с заголовками и кукисами.

Параметры

Как прописывать параметры в запросах; начнем с параметров пути.

Пути

Применяем pathParam(parameter-name, value):

@Test
public void whenUsePathParam_thenOK() {
    given().pathParam("user", "eugenp")
      .when().get("/users/{user}/repos")
      .then().statusCode(200);
}

Чтобы добавить несколько параметров — метод pathParams():

@Test
public void whenUseMultiplePathParam_thenOK() {
    given().pathParams("owner", "eugenp", "repo", "tutorials")
      .when().get("/repos/{owner}/{repo}")
      .then().statusCode(200);

    given().pathParams("owner", "eugenp")
      .when().get("/repos/{owner}/{repo}","tutorials")
      .then().statusCode(200);
}

В этом примере использованы проименованные параметры пути, а можем также и неименованные, или даже сочетать их:

given().pathParams("owner", "eugenp")
  .when().get("/repos/{owner}/{repo}", "tutorials")
  .then().statusCode(200);

Получим URL https://api.github.com/repos/eugenp/tutorials. Как видим, неименованные параметры — индексные.

Параметры запроса

Далее посмотрим, как указывать параметры запроса через queryParam():

@Test
public void whenUseQueryParam_thenOK() {
    given().queryParam("q", "john").when().get("/search/users")
      .then().statusCode(200);

    given().param("q", "john").when().get("/search/users")
      .then().statusCode(200);
}

Метод param() работает как queryParam() с GET-запросами.

Если нужно несколько параметров, можно или «зачейнить» несколько методов queryParam() цепочкой, или добавить параметры в метод queryParams():

@Test
public void whenUseMultipleQueryParam_thenOK() {
 
    int perPage = 20;
    given().queryParam("q", "john").queryParam("per_page",perPage)
      .when().get("/search/users")
      .then().body("items.size()", is(perPage));   
     
    given().queryParams("q", "john","per_page",perPage)
      .when().get("/search/users")
      .then().body("items.size()", is(perPage));
}

Параметры формы

Параметры формы указываются через formParam():

@Test
public void whenUseFormParam_thenSuccess() {
 
    given().formParams("username", "john","password","1234").post("/");

    given().params("username", "john","password","1234").post("/");
}

Метод param() работает как formParam() в POST-запросах.

Заметим, что formParam() добавляет заголовок Content-Type со значением “application/x-www-form-urlencoded“.

Заголовки 

Заголовки в запросах кастомизируются при помощи header():

@Test
public void whenUseCustomHeader_thenOK() {
 
    given().header("User-Agent", "MyAppName").when().get("/users/eugenp")
      .then().statusCode(200);
}

В этом примере применили header() чтобы задать заголовок User-Agent.

Также можно добавить заголовок с несколькими значениями в том же методе:

@Test
public void whenUseMultipleHeaderValues_thenOK() {
 
    given().header("My-Header", "val1", "val2")
      .when().get("/users/eugenp")
      .then().statusCode(200);
}

Запрос с двумя заголовками, My-Header:val1 и My-Header:val2.

Чтобы добавить несколько заголовков — метод headers():

@Test
public void whenUseMultipleHeaders_thenOK() {
 
    given().headers("User-Agent", "MyAppName", "Accept-Charset", "utf-8")
      .when().get("/users/eugenp")
      .then().statusCode(200);
}

Добавляем кукисы

Кастомный cookie добавлен в запрос методом cookie():

@Test
public void whenUseCookie_thenOK() {
 
    given().cookie("session_id", "1234").when().get("/users/eugenp")
      .then().statusCode(200);
}

Или через Builder:

@Test
public void whenUseCookieBuilder_thenOK() {
    Cookie myCookie = new Cookie.Builder("session_id", "1234")
      .setSecured(true)
      .setComment("session id cookie")
      .build();

    given().cookie(myCookie)
      .when().get("/users/eugenp")
      .then().statusCode(200);
}

Аутентификация

Далее рассмотрим тестирование и валидацию безопасности API. Поддерживаются базовая, дайджест, через форму, и через OAuth 1 и 2.

Базовая

Базовая схема аутентификации (подробное описание в стандарте по ссылке) предполагает отправку user id и пароля, закодированных в Base64.

given().auth()
  .basic("user1", "user1Pass")
  .when()
  .get("http://localhost:8080/spring-security-rest-basic-auth/api/foos/1")
  .then()
  .assertThat()
  .statusCode(HttpStatus.OK.value());

Упреждающая (Preemptive)

По умолчанию REST Assured ждет когда сервер пришлет вызов (challenge), перед тем как отправить user id и пароль (здесь подробнее). Это иногда вызывает проблемы, например если сервер по умолчанию отправляет форму логина, а не вызов. Поэтому в REST Assured есть поддержка упреждающей аутентификации:

given().auth()
  .preemptive()
  .basic("user1", "user1Pass")
  .when()
  // ...

— отправлены юзернейм и пароль без ожидания Unauthorized-ответа.

Проверка способности сервера корректно обрабатывать такие события очень важна в тестировании безопасности, поэтому стандартно применяют эту схему, чтобы не усложнять процесс, и не создавать дополнительные запросы.

Дайджест

Хотя по стандартам эта схема считается слабой, в ней есть преимущества, например пароль не отправляется в текстовом виде. 

Реализация дайджест-аутентификации в REST Assured аналогична примеру выше:

given().auth()
  .digest("user1", "user1Pass")
  .when()
  // ...

Однако, в данное время REST Assured поддерживает только аутентификацию запрос-ответ для такой схемы, так что preemptive() теперь не получится использовать.

Через форму

Множество веб-сервисов используют HTML-формы для аутентификации пользователей, то есть с заполнением полей ввода и отправкой. Когда пользователь заполнил форму, браузер выполняет POST-запрос. Обычно форма связана с эндпойнтом, который она вызовет своим action-атрибутом, и каждое input-поле соответствует параметру формы, отправленному в запросе.

Если форма логина простая и эти правила соблюдены, можно положиться на REST Assured:

given().auth()
  .form("user1", "user1Pass")
  .when()
  // ...

Но это неоптимальный подход, потому что REST Assured должен выполнить дополнительный запрос и обработать HTML-ответ, чтобы найти поля.

Также надо помнить, что обработка может быть неудачной, например если страница большая и сложная, или если сервис сконфигурирован так, что контекстный путь не включен в action-атрибут.

Поэтому лучшим решением является настроить свою конфигурацию, эксплицитно указывая все три обязательных поля:

given().auth()
  .form(
    "user1",
    "user1Pass",
    new FormAuthConfig("/perform_login", "username", "password"))
  // ...

Кроме этих базовых конфигураций, в REST Assured есть функциональность:

  • Определять поле CSRF-токена на странице
  • Дополнительные поля форм в запросе
  • Логировать процесс аутентификации

OAuth 

OAuth является фреймворком для авторизации, и не обладает механизмами аутентификации пользователя. Однако он может применяться в качестве базы для настройки протокола аутентификации/идентификации (как например в OpenID Connect).

OAuth 2.0

REST Assured позволяет конфигурировать токен доступа OAuth 2.0 для запроса защищенного ресурса:

given().auth()
  .oauth2(accessToken)
  .when()
  .// ...

Библиотека никак не поможет в получении токена доступа, так что придется это делать самому. Для пользовательских путей Client Credential и Password это просто, поскольку токен получают просто предоставив соответствующие пользовательские данные. С другой стороны, автоматизация пользовательского пути Authorization Code будет сложнее, и придется применять другие инструменты. 

OAuth 1.0a

Для OAuth 1.0a REST Assured предоставляет метод, получающий Consumer Key, Secret, Access Token и Token Secret для доступа к защищенному ресурсу:

given().accept(ContentType.JSON)
  .auth()
  .oauth(consumerKey, consumerSecret, accessToken, tokenSecret)
  // ...

Этот протокол требует ввода от пользователя, поэтому получение последних двух полей — нетривиальная задача. 

Отметим, что нужно будет добавить зависимость scribejava-apis в проект, чтобы воспользоваться функциями OAuth 2.0-2.5, или OAuth 1.0.

Весь код проекта и другие примеры: GitHub

Официальный сайт REST-assured

Источник туториала

***

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас