В предыдущей статье я поделился своим опытом автоматизации на Robot Framework. Теперь же речь пойдет о несколько другом подходе к тестированию API для проекта на Kotlin.

Воспользовавшись свободой выбора стека технологий и опираясь на желание попробовать «в бою» что-то новое, я обратился к Rest-Assured. Не без некоторых сложностей мы с коллегами запустили тесты, а по итогам освоения подхода записали его в список ключевых для подобного рода задач.

image

(изображение используется на правах пародии)

Началось все с того, что поступил запрос на автоматизацию тестирования API на одном из новых проектов в сегменте Ad-tech. Мы делали Campaign Manager, DMP и несколько интеграций со сторонними системами. А перед тестерами стояла предельно простая задача: автоматизировать смоук-тестирование для дальнейшей интеграции в CI и постоянного мониторинга состояния данного API, так как на него завязываются сторонние системы и корректность работы критична. Проверка бизнес-логики в данном случае не требовалась, поскольку тестируемый сервис по сути является прокси-интерфейсом.

Почему Rest-Assured?


С точки зрения автоматизации тестирования мы успели поработать в самых разных областях — UI, web, mobile, desktop, бэкенд, REST API, SOAP. Прошлый проект дал нам довольно большой опыт на Robot Framework, поэтому логичнее всего было бы использовать именно его. Большинство в команде тестирования с ним знакомы, и если потребуется срочная замена специалиста, сделано это будет быстро и фактически безболезненно. Но было принято иное решение.

Во-первых, сам проект был написан на Kotlin и собирался с помощью Gradle. При этом на стадии проектирования автотесты в отдельный проект решили не выделять. Как было отмечено в комментариях к предыдущей статье, склеить Java (Kotlin) и Robot Framework — большая боль, поэтому не было никакого смысла обращаться к RF. Плюс, оглядываясь на работу с роботом, мне было интересно попробовать иной подход. Тем более что на данном проекте мы могли самостоятельно выбирать стек технологий — бизнес не ставил своих условий.

В поисках идей я обратился к нашей команде разработки, а также коллегам из тестирования, и CTO посоветовал присмотреться к Rest-Assured (rest-assured.io). После того, как я почитал документацию и примеры тестов в открытых источниках, подход, предлагаемый Rest-Assured, показался мне очень привлекательным. Он подразумевает прием ответа от бэкенда в виде JSON, который мы десериализуем в старые добрые объекты Java.

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

Для точности отмечу, что приблизительно тот же failproof-тест можно было бы получить на RF, но немного другим путем. Однако рассказ сегодня не об этом. Лично мне был удобнее и понятнее заход со стороны Rest-Assured с сущностью, у которой есть определенные поля и методы.

Проба пера


Прежде чем начать использовать стек на реальном проекте, я решил опробовать его на небольшом CRUD-сервисе. Чтобы сразу не практиковать собственные ошибки, решил поискать best practises по данному стеку и достаточно быстро нашел статью Филиппа Хауера, где он отразил свой опыт автоматизации на Rest-Assured. После изучения статьи написать рабочую версию тестов моего сервиса не составило труда. Они получились простые, легко читаемые и понятные.

В бой!


Проект стартовал, и когда первые структуры ответов были описаны, началась подготовка тестовой документации и написание автотестов. Для понимания общей картины приведу полный стек автотестов:

  • Java,
  • JUnit4 — исполнение и параметризация тестовых сценариев (стандартная аннотация @Parametrize),
  • Rest-Assured — построение и выполнение запросов,
  • AssertJ — валидация полученных значений,
  • Allure — построение отчетности,
  • Gradle — сборка,
  • Jackson — десериализация.

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

image

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

image

Rest-Assured позволяет десериализовать ответ в требуемый класс. Полученный ответ раскладывается в заранее известную структуру с помощью Jackson. Классы для десериализации выглядят максимально просто — описываются все ожидаемые поля с их типами.

image

Дальше продолжается простая работа с объектами и ассертации конкретных полей и их значений.
Самое сложное и неочевидное, с чем мне довелось столкнуться в данным подходе, это невозможность передать в Rest-Assured в качестве одного из параметров null (результат — падение в NullPointerException). Судя по всему, это известная проблема, но исправлять ситуацию разработчик не собирается, рекомендуя либо отправлять поле пустым, либо не отправлять его вовсе. С этой проблемой мы столкнулись уже на том этапе, когда основа проекта была готова и оставалось только дописать тестовые классы. Поэтому просто немного подкорректировали свой код.

В целом подход мне понравился. Любопытно, что через некоторое время он начал применяться и на проекте другого заказчика (правда, не с нашей подачи). А собранный стек для автоматизации тестирования API (JUnit + AssertJ + Rest-Assured) мы вынесли в категорию ключевых для проектов на Java/Kotlin. В Python его тянуть будет нелогично, поскольку лучше, чтобы компетенции разработки и тестирования пересекались.

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

Итоги


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

Автор статьи: Дмитрий Мастеров

P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на наши страницы в VK, FB или Telegram-канал, чтобы узнавать обо всех наших публикациях и других новостях компании Maxilect.

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


  1. hMartin
    22.10.2018 21:26

    Советую поглядеть в сторону feign(okhttp3 в кач-ве клиента) + jackson в качестве decoder/encoder.
    Ресташшурд тащит рантайм груви и выжирает 1-2гб оперативы, feign же — jaxrs-наоборот и является более простой и удобной вариацией Retrofit. Жрет мало ресов, шустрый, умеет в дженерики через наследование интерфейса, нет многословности с чейнингом, в общем, тестирование АПИ с ним весьма упрощается.

    Самое сложное и неочевидное, с чем мне довелось столкнуться в данным подходе, это невозможность передать в Rest-Assured в качестве одного из параметров null (результат — падение в NullPointerException).

    Можно подробнее кейс? Если это касается сериализации, то ObjectMapper Jackson'a имеет настройки пропускать или включать null, правда не связывался с ним в Ресташшурде.


    1. Maxilect_pr Автор
      23.10.2018 08:59

      Суть проблемы с null была не на этапе десериализации Jackson'ом, а в момент вызова API: Rest-Assured просто не давал отправить null. Судя по этому issue (https://github.com/rest-assured/rest-assured/issues/942), сталкивался с данной проблемой не только я.


  1. qwez
    23.10.2018 09:53

    Если честно, это дичь какая-то)
    Во-первых, по оформлению — код скринами, нет примеров самих тестов.
    Во-вторых, почему бы не сделать нормальные модели данных, а не эти ужасные классы BasePojo? Json файл можно очень быстро перегнать в класс Java с помощью плагина для IDE, и потом сделать нужные конструкторы или билдеры для формирования тестовых данных.
    В-третьих, вы Rest-Assured используете только для отправки запроса и десериализации? Это как их пушки по воробьям, имхо. Проще взять http-клиент и тот же Jackson, а не тащить «толстый» Rest-Assured, как правильно заметили в предыдущем комменте.

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


    1. Maxilect_pr Автор
      23.10.2018 10:42

      Отвечу по пунктам.
      Во-первых, проект у нас под NDA, соответственно никаких живых примеров за рамками приведенных скриншотов я показать не могу. Скрины — лишь иллюстрация подхода, но не готовые модули для использования в своих проектах. Поэтому и в код их переводить не имеет смысла.
      Во-вторых, мы рассматривали вариант хранения статичных данных в JSON-ах на этапе выбора подхода, но предпочли генерировать все на лету, а не держать коллекции. На мой взгляд, это вопрос личных предпочтений.
      В-третьих, мы отталкивались от собственного удобства в рамках условий конкретного проекта.


      1. qwez
        23.10.2018 11:29

        мы рассматривали вариант хранения статичных данных в JSON-ах
        Я не про это. А про то, чтобы сделать обычные классы Java согласно модели данных передаваемых сообщений.
        public class Campaign {
            @JsonProperty("id")
            private int id;
            // etc...
        }

        И добавить конструкторы или билдеры, для «генерировать все на лету». Это было бы удобнее читать и править при необходимости.
        мы отталкивались от собственного удобства в рамках условий конкретного проекта
        про это бы написали. Какие еще рассматривали инструменты (кроме, разумеется, RF), почему не подошли.


        1. Maxilect_pr Автор
          23.10.2018 13:12

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

          2. Никакой масштабной оценки «всех инструментов на рынке», понятно, не было. Мы взяли первый же вариант, который подошел под требования и наши ожидания.


          1. hMartin
            23.10.2018 16:55

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

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

            2. Никакой масштабной оценки «всех инструментов на рынке», понятно, не было. Мы взяли первый же вариант, который подошел под требования и наши ожидания.


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

            Получается, вы выбрали ключевым(!!) инструментом продукт, на основе только лишь желания попробовать что-то новое(«первый же вариант»).


            1. Maxilect_pr Автор
              24.10.2018 10:46

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

              2. Не вырывайте слова из контекста. Ключевым инструментом выбрали то, что: работает и (что важно) удовлетворяет требованиям/ожиданиям.