Начнём с азов

Почему бы нам просто не перестать тестировать вручную? 

  • Ведь это экономит время. Это очевидно, потому что прогнать автотест займет считанные секунды, а то и доли секунд, а вручную — десятки секунд, а то и несколько минут.

  • Автотест автоматически сгенерит нам документацию и отчётность

  • Затем автотест позволит нам снизить влияние человеческого фактора. Где автотест, который правильно настроен, всегда пройдёт без ошибок, человек может ошибиться. 

  • Высокий уровень точности, само собой, по сравнению с ручным тестированием.

Получается, автотесты — панацея, которую надо внедрять добровольно-принудительно? Это далеко не так.

В каких условиях нам НЕ пригодится автоматизация тестирования REST API

Случай №1 — на стадиях ранней разработки

Когда мы только-только получили ещё сырую API и она продолжает развиваться в процессе, лучше её тестировать вручную, пока она не обретёт какую-то стабильную версию. 

Есть, конечно же, исключения. Бывают случаи, когда документация уже написана и, в принципе, API дальше меняться не будет. Тогда, конечно, можно начать писать автотесты параллельно с работой разработчика, который пишет эту API. Но, мне кажется, что такие ситуации всё-таки редки. 

Случай №2 — тесты, которые невозможно полностью автоматизировать.

Например, когда часть теста прогоняется вручную, а часть теста автоматизирована.

  • Это нам не даёт никакой экономии времени, по большому счёту.

  • Тесты становится сложнее поддерживать.

  • И их достоверность под большим вопросом.

Для чего тогда нам нужны автотесты API?

№1. Они позволяют нам упростить локализацию бага. Когда мы тестируем через UI, нам надо сначала локализовать баг, понять, где он вообще находится. Когда мы тестируем через API, мы уже понимаем, где он находится. 

№2. Снижение стоимости обнаружения бага достигается благодаря тому, что мы тестируем на уровне API, а не на уровне UI

№3. И проверка на соответствие требованиям с точки зрения надёжности, безопасности и производительности.

Что же нам надо знать об API, которое мы тестируем?

№1. Для начала убедимся, что API смотрит на нужный стенд и базу данных. Какой смысл в неактуальных данных? Пускай даже тест прошёл, но он смотрит не туда. 

№2. Затем проверяем корректность входа состояния HTTP. 

№3. Проверяем полезную нагрузку ответа. Здесь нам надо проверить правильность тела JSON: имена, типы и значение полей ответа.

№4. Проверяем заголовки ответа. Они влияют как на безопасность, так и на производительность. 

№5. И проверяем базовую работоспособность. Если операция была завершена успешно, но заняла слишком много времени, то тест не пройден.

Последствия 

Какие ошибки мы можем допустить, если не выполним все эти проверки? 

  • Неактуальные результаты тестирования. Когда мы смотрим, например, не на ту базу данных или не на тот стенд, мы получаем неактуальную информацию.

  • Пропуск багов, в том числе критичных.

  • Несоответствие требованиям безопасности и производительности. Пользователь может оказаться беззащитным перед злоумышленником, чего нам, конечно же, не надо. Пользователь может просто не дождаться, когда выполнится целевое действие или вовсе не сможет его выполнить.

Какие виды тестовых сценариев мы используем?

  • Основные позитивные тесты — это прохождение успешного сценария по умолчанию.

  • Расширенное позитивное тестирование, где мы добавляем дополнительные параметры.

  • И негативное тестирование. Здесь мы проверяем как валидные входные данные, так и недопустимые.

Также мне бы хотелось отметить, что всегда стоит уточнять. Коллеги с небольшим количеством опыта и те, кто только планирует начать работать в тестировании, обращаюсь к вам. Если что-то непонятно до конца — лучше лишний раз уточнить. Коллег с большим количеством опыта, прошу, пожалуйста, отвечайте на эти вопросы.

Коды состояния HTTP

Раз уж мы поговорили о том, что мы получаем в ответах, давайте вспомним и коды состояния HTTP. 

1хх — Informational. Сотые информационные коды нам говорят, что запрос получен и обработка продолжается.

2хх — Success. Двухсотые коды состояния говорят о том, что запрос был получен, успешно обработан и понят. 

3хх — Redirection. Трехсотые коды ответов говорят о том, что для выполнения запроса требуются ещё действия. 

4хх — Client Error: запросы имеют плохой синтаксис или не могут быть выполнены.

5хх — сервер не в состоянии выполнить недопустимый запрос.

Раз уж пойдёт речь о тестирование именно REST API, давайте же вспомним: что такое REST. 

Что такое REST?

REST или Representational State Transfer — это архитектурный стиль, который описывает взаимодействие компонентов распределённого приложения внутри сети. Другими словами, это такой набор ограничений, которые дают нам возможность в дальнейшем масштабировать приложение. 

RESTfull — это соответствие веб-службы требованиям REST.

REST — architecture
REST — architecture

Вот мы уже плавно подошли к самой сути статьи, поэтому выделим…

Четыре основных REST-assured метода

  • Given — позволяет узнать, что было передано в запросе.

  • When — с каким методом и на какой эндпойнт отправляем запрос.

  • Then — как проверяется пришедший ответ.

  • Body — тело, в нашем случае, ответа.

REST-assured вообще представляет из себя инструмент для тестирования API, и он встраивается в тесты на Java. И здесь конкретный пример, точнее схема, как оно выглядит. 

given()...
  .when()
  .get(someEndpoint)
  .then()
  .statusCode(200)
  .body();

Также стоит использовать паттерн Builder. 

Паттерн Builder

Зачем? Он позволяет сделать код понятнее, добавив подсказку того, что можно сконфигурировать, используя этот самый паттерн.

Пример паттерна.

Account account = new AccountBuilder()
                .withId(1)
                .withName("test")
                .withBalance(10)
                .build();

Если обратить внимание, то заметим, что REST-assured соответствуют паттерну Builder. 

Перейдём к примеру

Что нам потребуется для подготовки? 

В данном случае, у нас будет тестироваться API, которая принимает параметры поиска офисов и выдаёт в результате список информации про каждый офис. 

Будем использовать следующий стек: ЯП Java, библиотека REST-assured и jUnit5, и паттерн Builder. 

Учитываем все особенности проекта 

К примеру, в нашем случае особенность заключается в том, что мы не знаем, что происходит под капотом, поскольку мы обращаемся в закрытый сервис. Нам приходится довольствоваться только ответом, который он нам возвращается. 

Можно было бы порт прописывать напрямую в URL.

BASE_URL = "http://alfaххх-intХ:ХХХХ/ххххххх-ххххх-ххх-хххх-api"

Но вместо захардкоженного порта был написан метод, который ходит в оркестратор и получает актуальный порт и инстанс.

public class MarathonUtils {
    private static final String protocol = "http";

    public static String getServiceBaseUrl(String serviceMarathonPath) {

        Response response = RestAssured.given()
                .auth().basic(getPropertyOfName("mLogin"), getPropertyOfName("mPassword"))
                .baseUri(protocol + getPropertyOfName("mUri"))
                .basePath(serviceMarathonPath)
                .get("?embed=app.taskStats&embed=app.readiness")
                .then()
                .statusCode(200)
                .extract().response();
        //Возвращает первый найденный объект с информацией о работающем инстансе.
        Map<String, ?> state = response.getBody().path("app.tasks.find { it.state == 'TASK_RUNNING' }");

        if (state != null) {
            String host = state.get("host").toString();
            @SuppressWarnings("unchecked")
            ArrayList<String> ports = (ArrayList<String>) state.get("ports");
            String uri = response.getBody().path("app.env.SERVICE_NAME").toString();
            return String.format("%s://%s:%s/%s", protocol, host, ports.get(0), uri);
        }

        throw new NullPointerException("Отсутствуют работающие инстансы для " + serviceMarathonPath);
    }
}

Далее составляем JSON-schema по телу ответа API

Давайте посмотрим на неё. Это фрагмент схемы, который используется для тестирования данного проекта и данной API.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": [
    {
      "type": "object",
      "properties": {
        "pid": {
          "type": "string"
        },
        "uuid": {
          "type": "string"
        },

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

Что проверяет схема? 

Она проверяет тип данных. В зависимости от типа данных может применить дополнительные правила. Например, для типа данных Integer можно проверить минимальное-максимальное число, для массива Array — максимальное количество элементов, обязательность элементов (required) и так далее. 

А вот пример схемы сложнее, подробнее и информативнее.

{ 
    "$schema": "http://json-schema.org/draft-03/schema#", 
    "id": "urn:product_name#", 
    "type": "string", 
    "pattern":     "^\\S.*\\S$", 
    "minLength": 3, 
    "maxLength": 50, 
}

Примечание: JSON-schema используется, потому что гораздо проще проверять по ней, чем каждый отдельный параметр. API отдаёт нам информацию в большом объёме, почему бы не использовать JSON-schema, в которой мы можем прописать абсолютно всё?

Напишем автотест на базовую проверку

@ExtendWith(ReportPortalExtension.class)
public class BaseTest {
    static RequestSpecification requestSpecification;


    @BeforeAll
    static void setUp() {
        requestSpecification = RestAssured.given()
                .baseUri(BASE_URL_POS)
                .accept(ContentType.JSON);
    }

    public static ValidatableResponse checkStatusCodeInResponse(String url, int code, String schema)
    {
        return RestAssured.given(requestSpecification)
                .get(url)
                .then()
                .statusCode(code)
                .body(matchesJsonSchemaInClasspath(schema))
                .time(lessThan(1500L));
    }

    public void baseNegativeCheck(String posUrl) {
        checkStatusCodeInResponse(posUrl, CLIENT_ERROR_CODE, POS_API_SCHEMA_ERROR_JSON);
    }

    public void basePositiveHasItemsCheck(String path, String item, String posUrl, String schema) {
        checkStatusCodeInResponse(posUrl, SUCCESS_CODE, schema)
                .body(path, hasItems(item));
    }

    public void hasSizeCheck(String url, String path, int size) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path, hasSize(size));
    }

    public void equalToCheck(String url, String path, String schema, boolean operand) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, schema)
                .body(path, equalTo(operand));
    }

    public void positiveHasEntryCheck(String url, String path, String key, String value) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_JSON)
                .body(path, everyItem(hasItem(hasEntry(key, value))));
    }

    public void checkEveryItemHasSize(String url, int size, String path) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path, everyItem(hasSize(size)));
    }

    public void checkEveryItemLessOrEqualValue(String url, String path, String value) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path,everyItem(lessThanOrEqualTo(parseFloat(value))));
    }

    public void checkEveryArrayHasItem(String path, String item, String posUrl, String schema) {
        checkStatusCodeInResponse(posUrl, SUCCESS_CODE, schema)
                .body(path, array((containsString(item))));
    }
}

Здесь мы задаём requestSpecification, который представляет из себя преднастройку для запросов. Он будет применяться для всех тестов, которые будут наследоваться от данного класса. Мы в нём передаём URI, путь, методы, к примеру, accept — он у нас будет проверять тип данных. Здесь же мы видим применение уже знакомых нам given, then и body. 

В базовую проверку добавлена проверка на время отклика (1500 мс), что не заменяет в полной мере нагрузочное тестирование, но даёт возможность обнаружить проблему с производительностью на более раннем этапе тестирования. 

Тест.

  @DisplayName("Проверка соответствия первого офиса по городу \"Санкт-Петербург\"")
    @Positive
    @Test
    public void testSPB() {
        basePositiveHasItemsCheck("pid", "0819", getCityAndMetroAndLimitUrl("Санкт-Петербург", METRO_VALUE, LIMIT_1), POS_API_SCHEMA_JSON);
    }

А для проверки ответа описаны объекты:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class FullPosOfficesPojo {
    private String pid;
    private String uuid;
    private String mnemonic;
    private String title;
    private String address;
    private List<MetroPojo> metro;
    private Boolean embossingEnabled;
    private List<String> prodType;
    private List<KindsPojo> kinds;
    private Boolean close;
    private String clientAvailability;
    private String addressOfficial;
    private List<String> schedule;
    private List<Object> temporarySchedule;
    private List<ConstraintPojo> constraints;
}

Дальше…

Пишем тесты на позитивные и негативные кейсы

basePositiveHasItemsCheck("pid", "0819", 
getCityAndMetroAndLimitUrl("Санкт-Петербург", METRO_VALUE, LIMIT_1), POS_API_SCHEMA_JSON);
}

Позитивные кейсы помечены тегом positive. 

Первоначальный (сырой) вариант тестов
   /// Проверка параметров по умолчанию + город «Королёв"
    @Positive
    @Test
    public void testKorolev() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=1000&city=Королёв").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

    /// Проверка параметров по умолчанию + город "Санкт-Петербург"
    @Positive
    @Test
    public void testSPB() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

Позитивные проверки:

    @Positive
    @Test
    public void testSPB() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

Здесь мы используем функцию .assertThat — она выполняет сравнение.

         .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()

И видим уже знакомый нам паттерн builder. Обратите внимание, вот он паттерн:

RestAssured.given(requestSpecification)

И автотест соответствует паттерну builder.

Добавляем негативные кейсы.

@Negative
@Test
public void testCityWithoutOffices() {
    ValidatableResponse response = posApiProvider.getOffices(requestSpecificationOffices,
    getCityAndMetroRadiusAndLimitTestData("Байконур", METRO_VALUE, LIMIT_1));
    checkResponseStatusCodeAndScheme(response, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON);
    checkResponseHasSizeInPath(response, "$", 0);
}

Помечаем их тегом «negative». Не забываем, что надо писать тесты и на валидные, и на недопустимые данные.

Первоначальный (сырой) вариант тестов
/// Проверка запроса с пустым полем "город"
@Negative
@Test
public void testCityEmptyLine() {
    RestAssured.given(requestSpecification)
            .get("/offices?metroRadius=3500&city=").then()
            .assertThat()
            .statusCode(500);
}

/// Проверка запроса с пустым полем "радиус"
@Negative
@Test
public void testMetroRadiusEmptyLine() {
    RestAssured.given(requestSpecification)
            .get("/offices?metroRadius=&city=Москва").then()
            .assertThat()
            .statusCode(500);
}

Позже на данном проекте мы вынесли параметры в эндпойнты и добавили проверки для остальных полей запроса. В дальнейшем мы добавим тесты на проверку значений по схеме. Для чего с помощью того же паттерна builder будем подставлять значение в схему. 

Если у кого-то будут предложения о том, что можно сделать ещё, напишите в комментариях, с удовольствием изучу.

Конечно же, нигде не обходится без ошибок. 

Мои ошибки

Ошибки, в первую очередь, которые я допускал, это такие ошибки новичка — незнание того, о чём я вам сейчас рассказываю. 

Я начал автоматизировать, когда API был сырым и много менялся. Одну JSON-схему я переписывал минимум три раза. Про то, сколько раз приходилось менять код, нечего говорить.

И также хочется отметить, что для каждого теста следует подбирать подходящий метод, а не городить кучу кода. Пока это абстрактные слова, но сейчас я вам объясню на конкретном примере. 

Обычно мы проверяем равен ли один объект другому, используя equals. 

assertThat(retrievedEntity)
    .equals(expectedEntity,
        "number",
        "color",
        "date",
        "points",
        "price")
        …;

Такая проверка не будет работать, если был сгенерирован новый идентификатор для нового объекта. Следовательно, объекты разделяются по полям идентификатора. К счастью, можно указать методу /assertThat игнорировать определённые поля, во время проверки равенства, и нам не нужно будет грузить всю вот эту кучу кода (а полей может быть ещё больше).

А можно использовать…

assertThat(retrievedEntity)
    .isEqualToIgnoringGivenFields(expectedEntity, "id");

Правда, удобнее? Мы игнорируем ID и количество кода сокращается, поддерживать его становится намного проще и приятнее.

Конечно же, на начальном этапе автоматизации тестов это не столь очевидно ввиду незнания синтаксиса.

Достоинства и недостатки автоматизации API

Достоинств много. 

  • Быстрое получение обратной связи.

  • Аккуратное тщательное тестирование.

  • Высокое покрытие тестами.

  • Быстрое обнаружение ошибок.

  • Повторное использование тестов.

  • Более короткие сроки поставки.

  • Экономия времени и денег. 

Но у всего есть, конечно же, своя цена. Цена внедрения автотестов API — это затраты времени на обучение. Например, я погружался в тестирование REST API с полного нуля.

  • Обучение заняло у меня один месяц. 

  • Проектирование же, с вопросами более опытным коллегам, заняло у меня одну неделю. 

  • Создание автотестов на данный момент — 2 недели, но оно продолжается и сейчас, и будет продолжаться, я думаю, ещё долго. 

  • Актуализация автотестов будет длиться до самого конца, пока используется это API.

У меня было примерное понимание, что такое REST, но как автоматизировать проверки — никакого понимания у меня не было. Конечно же, я не могу сказать, что теперь я знаю о нём всё. Но, по крайней мере, могу писать тесты и как-то развиваться дальше. 

Чек-лист

Давайте же подводить итоги.

Подготовка — на что следует обратить внимание, когда мы готовимся внедрять автотесты на REST API.

  • Для начала, оцените необходимость внедрения API: нужно оно вообще вам или нет. 

  • Затем изучите тестируемое API, так как нельзя тестировать то, чего ты не понимаешь. 

  • Следует учесть особенности вашего кейса.

  • Подготовить окружение.

  • И можно переходить к основным действиям.

Основные действия.

Для начала составьте JSON-schema. Затем напишите тесты:

  • на базовую проверку;

  • на расширенные позитивные кейсы;

  • и на негативные тесты, как с валидными, так и с невалидными данными, используя для этого паттерн builder, библиотеку REST-assured и jUnit5.

В тестах:

  • Обратите внимание на то, что API смотрит на нужный стенд и базу данных.

  • Проверьте корректность кода состояния HTTP.

  • Проверьте полезную нагрузку ответа.

  • Проверьте заголовки ответа.

  • Проверьте базовую работоспособность.

Ну что ж, в принципе всё.

За помощь в подготовке и корректировке кода, благодарю моих коллег QA. Была ли вам полезна статья, почерпнули ли для себя что-то новое? 


Рекомендованные статьи:

Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.

Комментарии (2)


  1. Saveur
    13.07.2023 05:42
    -1

    Что вы там тестируете? Вы даже не хотите организовать схему подтверждения электронной почты ваших клиентов. Папка СПАМ на 90% заполнена Альфабанком. Реклама, отчеты о собраниях, акции и т.д. и т.п. Если кто-то из ваших клиентов указал мою почту, то почему я должен с этого иметь проблемы и тратить время? Пусть он её сначала подтвердит, и только после подтверждения отправляйте вашу "конфиденциальную информацию" ему. Мне не надо.

    УВЕДОМЛЕНИЕ О КОНФИДЕНЦИАЛЬНОСТИ: Это электронное сообщение и любые документы, приложенные к нему, содержит конфиденциальную информацию. Настоящим уведомляем Вас о том, что если это сообщение не предназначено Вам, использование, копирование, распространение информации, содержащейся в настоящем сообщении, а также осуществление любых действий на основе этой информации, строго запрещено.


  1. otryvanov
    13.07.2023 05:42

    Привет. А зачем query параметры указывать в урле?

    .get("/offices?metroRadius=1000&city=Королёв")

    можно же вот так

    .get("/offices")
    .queryParam("metroRadius", 1000)
    .queryParam("city", Королёв)

    а еще лучше указывать переменные и из 2х тестов получаем 1 параметризованный. Меньше кода и легче поддерживать.