Привет, Хабр. Меня зовут Николай Борисенко. Я специалист по автоматизации тестирования в ОК, и я продолжаю наш рассказ о генерации тестов на основе спецификации API.

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

Автоматизация API-методов

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

Проанализировав весь API-проект я понял что можно генерировать автотесты на однотипные проверки, в этой статье мы посмотрим как я реализовал генерацию автотестов в нашем проекте. Но прежде чем что‑либо генерировать, надо понять, что именно можно генерировать. Давайте для примера рассмотрим следующие методы и представим их в виде таблицы:

Комментарий к видео

Загрузка видео

Параметры:

Видео ID — обязательный параметр.

Текст — обязательный параметр.

Параметры:

Название — необязательный параметр.

Путь к файлу — обязательный параметр.

Дополнительная информация — необязательный параметр.

Тип сессии: Анонимная сессия

Тип сессии: Пользовательская сессия

Просмотр видео

Отправка другу видео

Параметры:

Видео ID — обязательный параметр.

Параметры:

ID друга — обязательный параметр.

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

Текст — необязательный параметр.

Тип сессии: Без сессии.

Тип сессии: Пользовательская сессия.

Во многом они схожи, поэтому для примера разберем два из них: на комментирование и загрузку видео.

Комментарий к видео

Загрузка видео

Параметры:

Видео ID — обязательный параметр.

Текст — обязательный параметр.

Параметры:

Название — необязательный параметр.

Путь к файлу — обязательный параметр.

Дополнительная информация — не обязательный параметр.

Тип сессии: Анонимная сессия.

Тип сессии: Пользовательская сессия.

Структура методов

Каждый метод состоит из нескольких смысловых блоков:

  • Название.

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

  • Тип сессии. У каждого метода должна быть указана сессия, в которой он вызывается. Например, у метода комментирования видео — это анонимная сессия, а у загрузки видео — пользовательская. Также стоит заметить, что некоторые методы можно вызывать без сессии. При этом, тут всё работает также, как и с параметрами — если не указать тип сессии или указать некорректный, тогда мы должны получить исключение.

Виды тестов для генерации

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

Исходя из структуры методов, мы можем генерировать несколько видов тестов. Среди них:

  • вызовы методов в некорректных сессиях;

  • вызовы метода без одного обязательного параметра;

  • вызовы метода без нескольких обязательных параметров.

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

Вместе с тем, надо понимать, что набор шаблонных проверок в каждом конкретном случае будет отличаться — в вашем проекте они могут и, скорее всего, будут другими.

Инструменты для реализации генератора автотестов

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

Для этого можно использовать разные инструменты. Вместе с тем, у нас есть ряд критериев для выбора решения.

Языки программирования

Дляреализации нашего проекта подойдет любой язык, который поддерживает Reflection — механизм, который позволяет взаимодействовать с классами во время выполнения программы, даже если класс заранее неизвестен. С помощью Reflection можно:

  • узнать класс объекта;

  • создать экземпляр класса;

  • получить информацию о его полях, методах и конструкторах;

  • вызвать метод объекта;

  • получить и установить значения поля объекта.

То есть он позволяет гибко получать и взаимодействовать с информацией о классе.

Reflection поддерживают такие языки, как Java, Python, Kotlin, Go и другие.

API-клиент

Для реализации генератора нужен любой API‑клиент, написанный на языках, у которых есть механизм Reflection.

Механизм Reflection позволит нам взаимодействовать с нашим API‑клиентом, следующим образом:

  • Создать экземпляр объекта класса.

  • Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах.

  • Вызвать метод объекта по имени.

  • Получить и установить значение поля объекта по имени.

  • Узнать класс объекта.

CI/CD

С помощью CI/CD можно создавать конвейеры выполнения кода и настраивать его сборку в автоматическом режиме. Например, мы знаем, что у нас апдейт API‑библиотеки происходит в понедельник. Мы можем настроить конвейер так, чтобы после обновления API‑библиотеки запускался наш генератор и генерировал тесты на новую версию библиотеки.

В качестве таких решений можно использовать TeamCity, Jenkins, GitLab, GoCD, Azure DevOps, GitHub Actions, Bitbucket CI и другие инструменты.

Удаленный репозиторий

Для реализации фичи также требуется удаленный репозиторий под хранение кода генератора. К числу таких относятся Bitbucket, Git, GitLab.

Примечательно, что почти все репозитории являются оберткой над git, и обычно мы взаимодействуем с ними через консоль. Но во время генерации все должно быть автоматизировано, поэтому для этого мы будем использовать вспомогательные библиотеки для работы с git — например, JGit, Pygit2, GitPython, GO‑Git.

Фреймворки для автотестов

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

Пример фреймворков для автотестов — Junit, Pytest, TestNG.

Наш стек

Для реализации генератора автотестов был выбран следующий стек:

  • Язык программирования с Reflection — Java;

  • API‑клиент — кастомный API‑клиент написанный на языке Java;

  • CI/CD — TeamCity;

  • удаленный репозиторий — Bitbucket;

  • Работа с git — JGit;

  • Фреймворк для автотестов — Junit5.

Список методов

Вернемся к списку методов.

Комментарий к видео

Загрузка видео

Параметры:

Видео ID — обязательный параметр.

Текст — обязательный параметр.

Параметры:

Название — необязательный параметр.

Путь к файлу — обязательный параметр.

Дополнительная информация — необязательный параметр.

Тип сессии: Анонимная сессия.

Тип сессии: Пользовательская сессия.

Просмотр видео

Отправка другу видео

Параметры:

Видео ID — обязательный параметр.

Параметры:

ID друга — обязательный параметр.

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

Текст — необязательный параметр.

Тип сессии: Без сессии.

Тип сессии: Пользовательская сессия.

Преобразуем их в спецификацию нашего API‑клиента.

ApiMethodEndpoint(

ApiMethod.COMMENT_HEISENBUG_VIDEO

)

commentHeisenbugVideo(

@RestParam(value = "videoId", required = true)

Long var1, 

@RestParam(value = "text", required = true)

String var2

)

@ApiMethodEndpoint(

ApiMethod.UPLOAD_HEISENBUG_VIDEO

)

uploadHeisenbugVideo(

@RestParam(value = "name", required = false)

String var1, 

@RestParam(value = "pathToFile", required = true)

File var2, 

@RestParam(value = "info", required = false)

VideoInfo var3

)

@ApiMethodEndpoint(

ApiMethod.WATCH_HEISENBUG_VIDEO

)

watchHeisenbugVideo(

@RestParam(value = "videoId", required = true)

Long var1

)

@ApiMethodEndpoint(

ApiMethod.SEND_HEISENBUG_VIDEO

)

sendFriendHeisenbugVideo(

@RestParam(value = "friendId", required = true)

Long var1, 

@RestParam(value = "videoId", required = true)

Long var2, 

@RestParam(value = "text", required = false)

String var3

)

Теперь попробуем разобраться, как с помощью Java Reflection достать информацию из API-клиента и превратить ее в автотест.

Начнем с аннотации @ApiMethodEndpoint, которая есть у каждого метода. Работа этой аннотации рассматривалась в моей статье.

@ApiMethodEndpoint 

Аннотация @ApiMethodEndpoint внутри себя содержит несколько обязательных параметров:

  • название категории метода;

  • название метода;

  • тип сессии, в которой нужно вызывать метод.

@ApiMethodEndpoint(

ApiMethod.COMMENT_HEISENBUG_VIDEO(

"heisenbug", 

"commentHeisenbugVideo",

ApiProtectionLevel.ANONYM_SESSION_REQUIRED

)

)

@ApiMethodEndpoint(

ApiMethod.UPLOAD_HEISENBUG_VIDEO(

"heisenbug", 

"uploadHeisenbugVideo",

ApiProtectionLevel.SESSION_REQUIRED

)

)

@ApiMethodEndpoint(

ApiMethod.WATCH_HEISENBUG_VIDEO(

"heisenbug", 

"watchHeisenbugVideo",

ApiProtectionLevel.SESSION_PROHIBITED

)

)

@ApiMethodEndpoint(

ApiMethod.SEND_HEISENBUG_VIDEO(

"heisenbug", 

"sendHeisenbugVideo",

ApiProtectionLevel.SESSION_REQUIRED

)

)

Так, чтобы получить категорию метода, с помощью Reflection нужно вызвать методы method.getAnnotation(ApiMethodEndpoint.class).value().getMethodName();.

По аналогии получается имя метода и тип сессии.

С помощью Java Reflection мы можем получить всю информацию, а после — взаимодействовать с ней. Например:

  • делать простое строковое представление полученной информации;

  • оборачивать в теги;

  • создавать сессии в зависимости от метода;

  • создавать названия автотестовых классов и методов.

Теперь нам надо вызвать метод для проверки.

Вызов любого метода в автотестах происходит с помощью переменной apiClient, у которой вызывается категория, которой принадлежит API-метод. Так же стоит заметить что переменная apiClient определена в базовом классе, от которого наследуются все автотесты и является экземпляром класса нашей API-библиотеки. Благодаря этому, с помощью Java Reflection мы можем взаимодействовать с методом: получить его имя и категорию, имея имя и категорию мы можем вызвать API-метод. 

Вызов API-метода через переменную apiClient будет выглядеть следующим образом:

"apiClient." + getServiceMethodByNameService(method.getDeclaringClass().getSimpleName() + "()." + method.getName();

После попытки сформировать вызов API-метода в автотесте можно заметить, что параметры никак не заполнены.

apiClient.getHeisenbugService().commentHeisenbugVideo(...);

apiClient.getHeisenbugService().uploadHeisenbugVideo(...);

apiClient.getHeisenbugService().watchHeisenbugVideo(...);

apiClient.getHeisenbugService().sendHeisenbugVideo(...);

Их нужно заполнить.

У каждого параметра есть аннотация RestParam, в которой хранится его имя и информация о том, обязательный это параметр или нет. Также у самого параметра есть тип. 

commentHeisenbugVideo(

@RestParam(value = "videoId", required = true)

Long var1, 

@RestParam(value = "text", required = true)

String var2

)

uploadHeisenbugVideo(

@RestParam(value = "name", required = false)

String var1, 

@RestParam(value = "pathToFile", required = true)

File var2, 

@RestParam(value = "info", required = false)

VideoInfo var3

)

watchHeisenbugVideo(

@RestParam(value = "videoId", required = true)

Long var1

)

sendFriendHeisenbugVideo(

@RestParam(value = "friendId", required = true)

Long var1, 

@RestParam(value = "videoId", required = true)

Long var2, 

@RestParam(value = "text", required = false)

String var3

)

Эту информацию можно получить с помощью Java Reflection. Так, с помощью Java Reflection можно вызвать getAnnotation и получить:

  • имя — parameter.getAnnotation(RestParam.class).value();

  • указатель на обязательность — parameter.getAnnotation(RestParam.class).required();

  • тип — parameter.getType().getSimpleName().

Имея эту информацию, мы можем заполнить параметры метода.

@RestParam(value = "videoId", required = true)

Long var1, 

@RestParam(value = "text", required = true)

String var2

apiClient.getHeisenbugService().commentHeisenbugVideo((Long) 0, (String) "1234567");

@RestParam(value = "name", required = false)

String var1, 

@RestParam(value = "pathToFile", required = true)

File var2, 

@RestParam(value = "info", required = false)

VideoInfo var3

apiClient.getHeisenbugService().uploadHeisenbugVideo

((String) null,(File) new File(), (VidoInfo) null);

@RestParam(value = "videoId", required = true)

Long var1

apiClient.getHeisenbugService().watchHeisenbugVideo((Long) 0);

@RestParam(value = "friendId", required = true)

Long var1, 

@RestParam(value = "videoId", required = true)

Long var2, 

@RestParam(value = "text", required = false)

String var3

apiClient.getHeisenbugService().sendHeisenbugVideo

((Long) 0,(Long) 0,(String) null);

Можно заметить, что, если параметр необязательный, то мы заполняем его как null. Если обязательный — то мы заполняем его дефолтным значением. Об этом подробнее. 

Получение дефолтных значений для обязательных параметров реализовано с помощью конструкции switch(), в которую передается тип параметра. 

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

Список для примитивных типов данных

Список для ссылочных типов данных

switch (parameter.getType().getSimpleName()) {

       case "byte" -> "(byte) 0";

       case "short" -> "(short) 0";

       case "int", "long" -> "0";

       case "float", "double" -> "0.0";

       case "boolean" -> "false";

       default -> "null";

   }

String type = "";

switch (parameter.getType().getSimpleName()) {

       case "Double" -> type = "0.0";

       case "Float" -> type = "0.0F";

       case "Short" -> type = "(short) 0";

       case "Byte" -> type = "(byte) 0";

       case "Integer" -> type = "0";

       case "Long" -> type = "0L";

       case "Boolean" -> type = "false";

       case "String" -> type = "\"1234567\"";

       case "VideoInfo" -> type = 

«new VideoInfo(«12345», true)»;

       ...

       ...

       ...

       default -> type = "null";

   }

   return type;

}

Шаблоны автотестов

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

  • вызовы методов в некорректных сессиях;

  • вызовы метода без одного обязательного параметра;

  • вызовы метода без нескольких обязательных параметров.

При этом, для разных ситуаций нужны разные шаблоны тестов.

Каждый тест состоит из нескольких базовых компонентов. Это:

  • Аннотация, отмечающая, что метод является тестом. Например, аннотация @Test, @ParameterizedTest.

  • Тег. С помощью тегов можно помечать тест — например, что он относится к какому-то сервису или то, что он является автоматически сгенерированным. Кроме прочего, по тегам мы можем запускать тесты. 

  • Название. Название должно содержать в себе имя метода и тип выполняемой проверки.

  • Параметры. В параметры метода можно передавать автотестовых ботов или данные для параметризированных автотестов.

  • Логи. С помощью логов можно отследить прогресс выполнения теста, они видны в отчете после прогона на ферме.

  • Тип сессии. Эта часть отвечает за создание сессии (напомню — у нас их 3 типа: пользовательская, анонимная и без сессии).

  • Проверка. Обычно это assert из библиотеки Junit 5, в котором проверяется какое-то условие — например, что метод выдал верное исключение при вызове в некорректной сессии.

Теперь разберем несколько примеров шаблонов под разные вызовы.

Вызов без сессии

На примере автотестового метода, где вызывается API-метод без сессии, рассмотрим части, которые будут немного меняться в зависимости от метода:

  • Тег, в котором указано название метода.

  • Название метода, в котором указан API-метод и тип проверки.

  • Логи, в которых также указан тип проверки.

  • Создание сессии.

  • Вызов API-метода через apiClient.

  • Изменение константы в зависимости от типа проверки.

@Test
@Tag("commentHeisenbugVideo")
@Tag("badpass")
@Tag("generated_tests")
public void testHeisenbugCommentHeisenbugVideoNoSession() {
   LOGGER.intention("Проверим работу метода 'heisenbug.commentHeisenbugVideo' без сессии»);
   noSession();

   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().commentHeisenbugVideo(0, "1234567"),
      throwsException(ApiException.class, containsString(ERROR_NO_SESSION))
   );

   LOGGER.success("Успешно проверили работу метода 'heisenbug.commentHeisenbugVideo' без сессии");
}

Далее будем рассматривать только существенные отличия в зависимости от проверки. 

Вызов из анонимной сессии

Для анонимной сессии всё будет точно так же, но изменится вызов сессии на анонимную, и изменится тип проверки.

@Test
@Tag("uploadHeisenbugVideo")
@Tag("badpass")
@Tag("generated_tests")
public void testHeisenbugUploadHeisenbugVideoAnonymSession() {
   LOGGER.intention("Проверим работу метода 'heisenbug.uploadHeisenbugVideo' в анонимной сессии");
   bindAnonymSession();
   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().uploadHeisenbugVideo(null, new File(), null),
      throwsException(ApiException.class, containsString(ERROR_ANONYM_SESSION))
   );
   LOGGER.success("Успешно проверили работу метода 'heisenbug.uploadHeisenbugVideo' в анонимной сессии");
}

Вызов в пользовательской сессии

Для пользовательской сессии мы передадим автотестового бота в параметры, создадим пользовательскую сессию и изменим тип проверки.

@Test
@Tag("watchHeisenbugVideo")
@Tag("badpass")
@Tag("generated_tests")
public void testHeisenbugWatchHeisenbugVideoUserSession(TestBot testBot) {
   LOGGER.intention("Проверим работу метода heisenbug.watchHeisenbugVideo в пользовательской сессии");
   bindNewSession(testBot);

   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().watchHeisenbugVideo(0),
      throwsException(ApiException.class, containsString(ERROR_USER_SESSION))
   );

   LOGGER.success("Успешно проверили работу метода heisenbug.watchHeisenbugVideo в пользовательской сессии");
}

Вызов без одного обязательного параметра

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

@Test
@Tag("uploadHeisenbugVideo")
@Tag("badpass")
@Tag("generated_tests")
public void testHeisenbugUploadHeisenbugVideoOneRequiredParameter(TestBot testBot) {
   LOGGER.intention("Проверим работу метода 'heisenbug.uploadHeisenbugVideo' без единственного обязательного параметра");
   bindNewSession(testBot);

   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().uploadHeisenbugVideo(null, null, null),
      throwsException(ApiException.class, containsString(ONE_PARAMETER_ERROR))
   );

   LOGGER.success("Успешно проверили работу метода 'heisenbug.uploadHeisenbugVideo' без единственного обязательного параметра");
}

Вызов метода в пользовательской сессии без одного обязательного параметра отличается только константой в проверке.

Вызов без нескольких обязательных параметров

Но что делать, если у нас обязательных параметров не один, а несколько? Эту проблему можно решить с помощью JUnit5 аннотаций: ParametrizedTest и MethodSource. Они позволяют создавать провайдер аргументов и вызывать метод несколько раз, но уже с различным набором аргументов.

@Tag("sendFriendHeisenbug")
@Tag("badpass") @Tag("generated_tests")
@MethodSource("provideMissingParams")
@ParameterizedTest(name = "[{index}] friendId: {0}, videoId: {1}, text: {2}, errorMessage: {3}")
public void testSendFriendHeisenbugWithMissingParams(TestBot testBot, Long friendId, Long videoId, String errorMessage) {
   LOGGER.intention("Проверим работу метода 'heisenbug.sendFriendHeisenbug' при отсутствии обязательных параметров");
   bindNewSession(testBot);

   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().sendFrinedHeisenbug(friendId, videoId, null),
      throwsException(ApiException.class, containsString(errorMessage))
   );

   LOGGER.success("Успешно проверили работу метода 'heisenbug.sendFriendHeisenbug' при отсутствии 
обязательных параметров");
}

@Tag("sendFriendHeisenbug")
@Tag("badpass") 
@Tag("generated_tests")
@MethodSource("provideMissingParams")
@ParameterizedTest(name = "[{index}] friendId: {0}, videoId: {1}, text: {2}, errorMessage: {3}")
public void testSendFriendHeisenbugWithMissingParams(TestBot testBot, Long friendId, Long videoId, String errorMessage) { ... }

private static Stream<Arguments> provideMissingParams() {
   return Stream.of(
         Arguments.of(null, null, FRIEND_ID_MISSING_PARAM),
         Arguments.of(0, null, VIDEO_ID_MISSING_PARAM)
   );
}

Теперь, когда мы разобрались, как с помощью Java Reflection получать информацию и собирать ее в код автотеста, перейдем к самому генератору.

Шаги генерации тестов

Чтобы автотесты корректно генерировались и их можно было встроить в наши конвейеры, генерация должна состоять из нескольких шагов. Конвейер следующий:

  • клонирование проекта с автотестами;

  • анализ API‑клиента;

  • генерация тестов в проект;

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

  • создание Pull Request;

  • запуск автотестов.

Начнем с клонирования репозитория

Клонирование репозитория

У нас реализована кастомная схема генератора.

Клонирование происходит с помощью библиотеки JGit. Ее применение похоже на работу с Git через консоль, но с некоторым отличием. Так,

  • в консоли надо вызвать git clone, передать ссылку на проект и он склонируется в ту папку, в которой мы сейчас находимся;

  • здесь же порядок такой же, однако все это надо делать через код: надо вызвать метод Git.cloneRepository(), туда передать ссылку на проект и путь до директории, в которую попадает его клон.

С переходом в другую ветку всё еще проще. Через консоль вызываем git checkout и имя ветки. В Java‑коде все идентично, только происходит с помощью вызова методов.

Консоль

Код

git clone ssh://name.git

Git git = Git.cloneRepository()

     .setURI("ssh://name.git")

     .setDirectory(new File("tmp/autotest-api"))

     .call();

git checkout имя-ветки

git.checkout()

     .setName(branch)

     .call();

С помощью Java Reflection можно обратиться к API‑клиенту, собрать все его сервисы и вызвать генерацию на каждый метод:

for (Method method : ApiClient.class.getMethods()) {
   if (method.getReturnType().getName().contains("ServiceRemote")) {
       classes.add(method.getReturnType());
   }
}

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

List<? extends Class<?>> ignoreService = Arrays.stream(IGNORE_SERVICE.values()).map(IGNORE_SERVICE::getServiceRemoteClass).toList();
for (Class<?> clazz : ApiClientParser.getRemoteServicesFromApiClient()) {
   if (!ignoreService.contains(clazz)) {
   }
}

IGNORE_SERVICE является Enum, в котором перечислены классы, которые будут игнорироваться при генерации автотестов.

public enum IGNORE_SERVICE {
   IGNORE_ONE(One.class),
   IGNORE_TWO(Two.class),
   
   private final Class<?> serviceRemoteClass;


   IGNORE_SERVICE(Class<?> serviceRemoteClass) {
       this.serviceRemoteClass = serviceRemoteClass;
   }


   public Class<?> getServiceRemoteClass() {
       return serviceRemoteClass;
   }
}

Генерирование тестов в коде

Генерирование тестов осуществляется по принципу матрешки — мы последовательно вкладываем каждый компонент друг в друга.

Генерируем assert

assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().uploadHeisenbugVideo(null, null, null),
      throwsException(ApiException.class, containsString(ERROR_NO_SESSION))
   );

Генерируем метод

public void testHeisenbugUploadHeisenbugVideoNoSession() {
   LOGGER.intention("Проверим работу метода 'heisenbug.uploadHeisenbugVideo' без сессии");
   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().uploadHeisenbugVideo(null, null, null),
      throwsException(ApiException.class, containsString(ERROR_NO_SESSION))
   )
   LOGGER.success("Успешно проверили работу метода heisenbug.uploadHeisenbugVideo без сессии");
}

Генерируем теги 

@Test
@Tag("heisenbug")
@Tag("badpass")
@Tag("generated_tests")
public void testHeisenbugUploadHeisenbugVideoNoSession() {
   LOGGER.intention("Проверим работу метода 'heisenbug.uploadHeisenbugVideo' без сессии");
   assertThat(
      "Не получили ожидаемую ошибку при вызове метода",
      () -> apiClient.getHeisenbugService().uploadHBVideo(null, null, null),
      throwsException(ApiException.class, containsString(ERROR_NO_SESSION))
   );
   LOGGER.success("Успешно проверили работу метода 'heisenbug.uploadHeisenbugVideo' без сессии");
}

Генерируем тестовый класс

package ...
import  ...
/**
* Автосгенерированный тест на метод heisenbug.uploadHeisenbugVideo
* Документация: ...
*/
public class TestHeisenbugUploadHeisenbugVideoBadPass extends TestBase {
   private static final String ERROR_NO_SESSION = "SESSION_REQUIRED";
   @Test
   @Tag("heisenbug")
   @Tag("badpass")
   @Tag("generated_tests")
   public void testHeisenbugUploadHeisenbugVideoNoSession() { ... }
}

Отправка тестов в удаленный репозиторий

Наш сгенерированный тест нужно отправить в удалённый репозиторий.

Для этого возьмем снова библиотеку JGit и вызовем методы add, commit и push — все по аналогии с вызовом из терминала.

Консоль

Код

git clone ssh://name.git

Git git = Git.cloneRepository()

     .setURI("ssh://name.git")

     .setDirectory(new File("tmp/autotest-api"))

     .call();

git checkout имя-ветки

git.checkout()

     .setName(branch)

     .call();

git add "."

 git commit -m message

 git push

git.add().addFilepattern(".").call();

git.commit().setMessage(message).call();

git.push().call();

Создание Pull Request

Послеотправки метода в удаленный репозиторий с помощью API BitBucket можно создать Pull Request, для этого вызываем метод createPullRequest, в него передаем:

  • название;

  • описание;

  • две ветки: в которую хотим влить изменения, и из которой хотим влить изменения.

CreatePullRequest createPullRequest = new CreatePullRequest(
  title, 									
DESCRIPTION + forticomVersion + DESC_PART_2, 		      
new BranchRef(MAIN_BRANCH, BranchType.BRANCH), 		            
  new BranchRef(fromBranchName, BranchType.BRANCH) 		       
);
    stashApi.createPullRequest(PROJECT_KEY, REPOSITORY_SLUG, createPullRequest);

При создании Pull Request, запускаются автотесты и добавляется дежурный.

Собственный сервис для запуска автотестов

Для запуска автотестов мы используем собственный автотестовый API‑клиент. Также после прогонов можно посмотреть результаты запуска и диаграмму стабильности автотестов.

Благодаря диаграмме стабильности автотестов можно оценить их стабильность, в нашем случае она составляет 99,6%.

Полученная польза и планы на будущее

Разработка генератора автотестов на основе спецификаций API и его внедрение в наш конвейер покрытия методов тестами обеспечили комплексный результат.

  • Мы увеличили покрытие методов шаблонными проверками на 25%.

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

  • После внедрения генератора QA‑инженерам больше не надо проверять часть ситуаций. Теперь они могут уделять больше времени проверке сложных и специфических сценариев.

  • API‑методы часто меняются, и у QA‑инженеров не всегда есть время покрывать автотестами новые параметры, удалять неактуальные проверки и выполнять другие рутинные операции. Теперь это происходит автоматически.

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

Вместе с тем, мы не останавливаемся на достигнутом. Так, в перспективе мы планируем добавить еще ряд проверок. Среди них:

  • проверка разрешений приложений;

  • проверка, что нет ошибок при указании обязательных/необязательных параметров;

  • проверка граничных значений параметров;

  • проверка базовых ситуаций бизнес‑логики.

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


  1. dedmagic
    30.01.2025 10:38

    А как можно лицезреть первую часть? В тексте ссылки не видать, в профиле единственная публикация – эта.


    1. dedmagic
      30.01.2025 10:38

      Ага, нашёл, хотя и не без труда.

      Но лучше бы ссылка была в тексте :)