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

Привет, Хабр. Меня зовут Елизавета Андреева. Я инженер по автоматизации тестирования в ОК.Tech. Мы с коллегами в ОК разработали и внедрили автогенерацию API‑тестов, благодаря которой мы сокращаем ручную работу и время на написание однотипных автотестов, оставляем QA‑инженерам для покрытия только кейсы на бизнес логику. И в этой статье (которая станет первой в серии из двух частей) я начну рассказ о том, как мы реализовали наш генератор и каких результатов нам удалось достичь.

Немного контекста: автотесты в ОК

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

У нас есть 5 ключевых платформ, на каждую из которых мы пишем автотесты. По всем платформам насчитывается более 10 тысяч автотестов, если смотреть по классам, некоторые из которых содержат набор тестовых методов:

  • Web — около 3300;

  • API — около 3600;

  • Android — около 1600;

  • Mobile web — около 1300;

  • iOS — около 1200.

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

По количеству вызовов и приоритету платформа номер один для нас — API. Это связано с тем, что API используется как всеми платформами, так и внешними разработчиками. Соответственно, покрытие API для нас является первоочередной задачей.

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

  • Ресурсов не хватает. Под новые фичи продукта разработчики регулярно пишут и новые методы API. Одновременно старые методы могут изменяться или удаляться — соответственно, автотесты требуют постоянной актуализации. Как результат, ресурсов QA‑инженеров не хватает, так как они работают не только с API, а пишут тесты еще и на другие 4 платформы, а также занимаются ручными задачами.

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

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

Как решение, мы в ОК внедрили автогенерацию тестов, которая одновременно позволяет:

  • покрыть шаблонные проверки;

  • освободить время QA‑инженеров на сложные ручные кейсы, на которые раньше могло оставаться мало времени;

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

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

Ключевые компоненты

В нашей системе есть три ключевых компонента:

  • проект с нашим API, где описаны методы;

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

  • проект с генератором.

Примечание: Мы работаем с REST API, но реализованное нами решение подойдет и для других современных API.

«Симбиоз» проектов работает следующим образом:

  • с помощью проекта с генератором создаются тесты в проект с автотестами API;

  • проект с генератором и проект с автотестами API используют проект с нашим API как библиотеку и делают вызовы через наш кастомный API‑клиент.

Теперь остановимся на каждом из ключевых компонентов по отдельности.

Проект с нашим API

Начнём с нашего проекта с API, с помощью спецификации которого мы смогли генерировать тесты. Проект написан на Java. В нем находится наш API‑клиент. Мы не используем готовые решения и не собираемся от него отказываться, потому что у него есть ряд достоинств:

  • Его код понятен и разработчику, и QA‑инженеру. Вся информация о методах находится на верхних уровнях и тестировщик может легко заглянуть в проект, посмотреть, как устроен тот или иной метод, и даже внести изменения в проект под ревью разработчика.

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

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

Также наличие специальных аннотаций дает возможность генерировать документацию на основе спецификации из них. Для этого используется Reflection API. Мы собираем список методов API по известным аннотациям и создаем шаблоны, которые содержат название метода, параметры, их обязательность, тип разрешений, необходимые сессии и так далее.

Таким образом, благодаря специальным аннотациям, мы имеем довольно большой объем метаинформации. Это позволило нам задуматься над тем, чтобы реализовать автоматическое создание не только документации, но и самих тестов, поскольку для этого у нас тоже достаточно данных. Давайте рассмотрим более подробно, как устроено наше описание спецификации API, которое позволяет нам получать столько информации.

API-методы

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

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

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

 

Он будет получать в параметры:

  • название доклада (обязательно);

  • сам файл нужно откуда‑то получить, поэтому еще путь к файлу на компьютере;

  • по желанию дополнительную информацию.

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

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

Как добавить API-метод?

Чтобы добавить в наш API‑проект новый метод, разработчику нужно выполнить довольно простой чек‑лист. Надо:

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

  2. Описать класс с типом ответа, который мы получаем после вызова конкретного API‑метода (Response).

  3. Добавить endpoint, который предоставляет краткую информацию о методе (так как мы работаем с REST API).

Примечание: В отдельных ситуациях также бывает необходимым добавить новые типы данных. Это несложно, но нюанс всё же есть. Так, при генерации тестов кастомные типы данных сложнее заполнять, поскольку они не из стандартной Java‑библиотеки и их дефолтные значения неизвестны.

Давайте пойдем по пунктам нашего чек‑листа:

  • Находим интерфейс, который относится к нужному сервису (в нашем случае это HeisenbugService);

  • Добавляем в этот интерфейс метод uploadHeisenbugVideo.

public interface HeisenbugService {

  UploadHeisenbugVideoResponse uploadHeisenbugVideo(

Нам сразу необходимо указать тип возвращаемых данных. В нашем случае типом данных является ответ API‑метода, который нужно описать. Рассмотрим UploadHeisenbugVideoResponse:

public class UploadHeisenbugVideoResponse {

  private boolean success;

  public boolean isSuccess() {
    return success;
  }

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

Далее метод мы помечаем нашей специальной аннотацией ApiMethodEndpoint.

public interface HeisenbugService {

  @ApiMethodEndpoint(ApiMethod.UPLOAD_HEISENBUG_VIDEO)
  UploadHeisenbugVideoResponse uploadHeisenbugVideo(

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

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

public interface HeisenbugService {

  @ApiMethodEndpoint(ApiMethod.UPLOAD_HEISENBUG_VIDEO)
  UploadHeisenbugVideoResponse uploadHeisenbugVideo(
    @RestParam(value = "name", required = true) String var1, 
    @RestParam(value = "pathToFile", required = true) File var2),
    @RestParam("info") VideoInfo var3));

Каждый параметр мы «оборачиваем» в аннотацию RestParam. Благодаря этому мы можем с помощью Java Reflection получать всю необходимую информацию о параметрах метода.

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

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface RestParam {

  String value();
   
  boolean required() default false;
  ...
}

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

@RestParam(value = "name", required = true) String var

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

Но все еще непонятно, где Васе взять метод для загрузки видео в ОК. Ответ прост — для этого у нас есть специальный интерфейс API‑клиента, в котором перечисляются все серисы: гейзенбаг, фото, видео и т. д.

public interface ApiClient {
  
  HeisenbugService getHeisenbugService();
  
  AnotherService getAnotherService();
  ...
}

Этот интерфейс мы уже и инициализируем в наших проектах с тестами.

Таким образом, чтобы вызвать наш метод в проекте с тестами, нужно:

  • Использовать уже предварительно проинициализированную переменную API-клиента, которая создается в нашем базовом тестовом классе;

  • Найти нужный сервис (в нашем случае это HeisenbugService);

  • Найти в сервисе метод для загрузки видео доклада.

И в коде это будет выглядит примерно так:

apiClient
  .getHeisenbugService()
  .uploadHeisenbugVideo(
    "API генератор", 
    new File("/HB"), 
    new VideoInfo("desc", true)
  )

Проект API-тестов

Теперь разберемся с тем, что из себя представляет проект с автотестами на наше API.

Этот проект существует также достаточно давно, вместе с появлением самого кастомного клиента, и тесты здесь писали QA-инженеры.

Наши сгенерированные тесты создаются по тем же правилам Code Style, используют тот же стек технологий, что и наши автотесты, написанные вручную, поэтому мы решили положить их в этот же проект. 

Шаги генерации в CI/CD

Мы не только автоматизировали генерацию тестов, но еще и автоматизировали сам этот процесс, включив его в единый пайплайн с апдейтом нашей API-библиотеки. Все шаги реализованы в TeamCity (далее TC), в сервисе, который мы используем в качестве основного инструмента для CI/CD (вы можете использовать любой другой). Схематично это выглядит так:

Давайте подробнее рассмотрим шаги:

  • Обновление версии API‑библиотеки в проекте с автотестами. Раз в неделю у нас происходит обновление нашей API‑платформы и вместе с этим запускается TC-задача в проекте с автотестами по обновлению версии API‑клиента.

    Проект с автотестами использует систему сборки Maven, где есть команда, которая помогает нам загрузить последнюю версию нашей библиотеки:

    mvn --update-snapshots "-DincludeProperties=our.api.version" -DallowSnapshots=false -DgenerateBackupPoms=false

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

    git status ‑porcelain pom.xml

    И если да, то заливать изменения в репозиторий.

  • Обновление версии API‑библиотеки в проекте Генератора. Далее по цепи вызовов запускается аналогичная задача обновления API‑клиента в проекте генератора. Разница в том, что здесь мы уже перешли на Gradle систему сборки, в которой нет встроенных команд для обновления версий библиотек. В качестве хранилища мы у себя используем Artifactory, и, чтобы здесь получить последнюю версию нашей библиотеки с API‑клиентом, мы просто используем API данного сервиса и Gradle‑задачи.

  • Сборка Генератора в JAR‑файл. Далее продолжаем вызовы по цепи и запускается сборка проекта генератора в JAR‑файл с помощью плагина Gradle (можете выбрать любой подходящий для вас). Мы сделали для этого шага отдельную задачу, потому что её удобно использовать ещё и в Pull Request'ах в проект генератора для проверки сборки билда без ошибок.

  • Запуск JAR‑файла. После сборки также по цепи запускается JAR файл.

  • Непосредственно сама генерация. Начинают выполняться шаги генерации, про которые подробнее будет рассказано во второй части статьи. Они описаны в Main классе проекта и кроме самой генерации включают в себя еще запуск тестов на ферме через клиент нашего сервиса для запуска тестов (обертка над TC, в которой мы храним конфигурации для наших тестов).

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

/За исключением задач по созданию и запуску JAR‑файла. Вторая опирается на Snapshot Dependencies и Artifact Dependencies. Последний нам нужен, чтобы забрать созданный JAR‑файл из задачи, которая соответственно создает JAR‑файл. Мы указываем, какой файл забрать из артефактов другой задачи и куда положить (например, в папку target), а также, что необходимо забирать его из одной цепи сборки, поэтому тут подключается Snapshot Dependencies настройка, и каждый раз, когда мы запускаем исполнение JAR‑файла, будет сначала запускаться его сборка в другой задаче и мы будем ждать оттуда артефакт.

Если на каком‑нибудь из шагов в цепи всех наших задач билд упал, то следующие задачи запущены не будут, а нашему дежурному по автотестам придет уведомление, что необходимо поправить билд и после запустить с того шага, где все остановилось.

Уведомления о падении в каждой задаче свои, добавляются самым последним шагом, который будет выполнен только если что‑то упало. Эти шаги опираются на переменную (в нашем случае env.status), которую мы задаем в последнем шаге задачи, необходимым по нашему замыслу для её успешного выполнения.

Например, если мы говорим про обновление нашей API-библиотеки, то последний шаг — commit и push в удаленный репозиторий.

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

Генератор не так давно был запущен, но мы уже получили результаты:

  • Увеличение покрытия автотестами на 25 %. Теперь каждый метод имеет минимальное покрытие. Однако при дальнейшем анализе покрытия методов, мы будем исключать пакет со сгенерированными тестами и смотреть, каким методам не хватает кейсов на бизнес логику. Для этого у нас есть свой инструмент, про который можно прочитать тут. (https://habr.com/ru/companies/odnoklassniki/articles/828 222/ )

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

  • Больше времени на сложные ручные кейсы. Мы покрыли шаблонные проверки, поэтому QA‑инженеры в освободившееся время могут пробовать более сложные ручные проверки, рассматривать больше корнер‑кейсов, на которые раньше могло не оставаться времени.

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

  • Быстрое изменение кода автоматически сгенерированных тестов. Если нужно внести какое‑то массовое изменение во все тесты, например, по Code Style мы хотим всем тестам добавлять тэг с названием API‑метода, и для того, чтобы актуализировать все тесты достаточно просто внести изменения в код генератора и он везде добавит тэг при следующей генерации.

В данной статье мы разобрали, как устроена наша спецификация и процесс генерации в CI/CD, а также, каких результатов нам удалось достичь. И еще давайте рассмотрим, как мы реализовали сам проект по автогенерации тестов на основе спецификации. И об этом уже во второй части нашей статьи. Она — здесь.

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


  1. Bruder_O_run
    15.12.2024 14:05

    Хороший процесс и хорошо описано. Даже подумалось может нужно это и у нас, надо посмотреть ))


  1. posledam
    15.12.2024 14:05

    Интересней генерация авто-тестов по спецификациям (сваггер, прото, етс.), слишком много завязано на конкретной реализации в бекенде, слишком большая сцепка. Второй вопрос, да конечно наверное приятно получить +25% "халявного" покрытия, но нет ли тут само-обмана? Если уж тут всё держится на аннотациях, то бекенд на этих же самых аннотациях и должен работать, т.е. тестировать надо всего лишь механизм аннотаций. Например, аннотация, требующая авторизации. Это должно работать уже просто потому, что аннотация добавлена, независимо от реализации метода. Даже если реализация вообще пустая, вызвать метод без авторизации нельзя. Ну или чего-то не понимаю, как будто лишняя работа, в холостую, ради "процентов покрытия", автотесты ради автотестов.