End-to-end тестирование микросервисов c Catcher +14


Добрый день! Я хотел бы представить новый инструмент для end-to-end тестирования микросервисов – Catcher
logo


Зачем тестировать?


Зачем нужно e2e тестирование? Мартин Фаулер рекомендует избегать его в пользу более простых тестов.


Однако, чем выше находятся тесты — тем меньше переписывать. Юнит тесты переписываются почти полностью. На функциональные тесты также приходится тратить свое время в случае серьезного рефакторинга. End-to-end тесты же должны проверять бизнес логику, а она меняется реже всего.


К тому же, даже полное покрытие тестами всех микросервисов не гарантирует их корректного взаимодействия. Разработчики могут неправильно имплементировать протокол (ошибки в наименовании/типе данных).


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


И тесты каждого из задействованных сервисов будут зеленые.


А зачем автоматическое тестирование?


Действительно. На моем предыдущем месте работы решили, что тратить время на разворачивание автоматических тестов это слишком долго, сложно и дорого. Система не большая (10-15 микросервисов с общей кафкой). CTO решил, что «тесты не важны, главное чтобы система работала». Тестировали вручную на нескольких окружениях.


Как это выглядело (общий процесс):


  1. Договориться с другими разработчиками (выкатка всех микросервисов, участвующих в новом функционале)
  2. Выкатить все сервисы
  3. Подключиться к удаленной кафке (двойной ssh в dmz)
  4. Подключиться к логам k8s
  5. Вручную сформировать и отправить кафка сообщение (слава богу json)
  6. Смотреть логи, пытаясь понять, заработало или нет.

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


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


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


Как выглядела регистрация нового тестового пользователя (приблизительно):


  1. Ввод всяких данных (имени, почты и т.п.)
  2. Ввод личных данных (адрес, телефон, всякая налоговая информация)
  3. Ввод банковских данных (собственно, банковские данные)
  4. Ответить на 20-40 вопросов (вы уже чувствуете боль?)
  5. Пройти идентификацию IDNow (на dev окружении, слава богу отключили, на stage это в районе 5 минут или больше, т.к. их сэндбокс иногда перегружен)
  6. На этом шаге требуется открытие счета в сторонней системе и через фрон-энд ничего не сделаешь. Нужно идти по ssh на кафку и работать мок-сервером (отправить сообщение, что счет открыт)
  7. Далее нужно идти на другой фронт-энд в личный кабинет модератора и подтвердить пользователя.

Супер, пользователь зарегистрирован! Теперь еще немного дегтя: для некоторых тестов нужно больше чем 1 тестовый пользователь. И иногда с первого раза тесты не проходят.


А как происходит проверка нового функционала и подтверждение со стороны бизнес-команды?
Все то же самое нужно повторить на следующем окружении.


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


Еще у некоторых разработчиков (обычно у фронт-энда) были проблемы с подключением к кафке. И с багом в терминале при строке 80+ символов (не все знали про tmux).


Плюсы:


  • не нужно ничего настраивать/писать. Тестируй прямо на запущенном окружении.
  • не требует высокой квалификации (могут делать более дешевые специалисты)

Минусы:


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

Как автоматизировать?


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


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


  • бэкэнд, который живет у вас в тестируемом окружении. В нем зашита логика тестирования, которая дергается через эндпоинты. Может быть даже частично автоматизирован благодаря взаимодейтсвию с CI.
  • Скрипт, с той же самой зашитой логикой. Разница только в том, что нужно зайти куда-нибудь и оттуда его запустить. Если вы доверяете своему CI, то вы даже можете запускать его автоматически.

Звучит неплохо. Проблемы?


Да, такие тесты пишутся на том, что знает тот, кто их пишет. Обычно это скриптовые языки вроде руби или питон, которые позволяют быстро и просто написать подобного рода вещь. Однако, иногда можно наткнуться на ворох баш-скриптов, Си или что-то более экзотическое (я потратил неделю, переписывая велосипед на баш скриптах на питон, потому что скрипты уже были не расширяемыми и никто толком не знал как они работают и что тестируют).
Пример проекта здесь


Плюсы:


  • автоматизация

Минусы:


  • возможны дополнительные требования к квалификации разработчиков (если они разрабатывают на Java, а тесты были написаны на Python)
  • написание кода для тестирование написаного кода (кто будет тестировать тесты?)

Есть что-нибудь готовое?


Конечно, достаточно посмотреть в сторону BDD. Есть Cucumber, есть Gauge.


Если кратко – разработчик описывает бизнес сценарий на специальном языке, после реализует шаги сценария в коде. Язык как правило человекочитаемый и предполагается что его будут читать/писать не только разработчики, но и менеджеры проектов.


Сценарии вместе с реализацией шагов также находятся в отдельном проекте и запускаются сторонними продуктами (Cucumber/Gauge/…).


Сценарий выглядит так:


Customer sign-up
================

* Go to sign up page

Customer sign-up
----------------
tags: sign-up, customer

* Sign up a new customer with name "John" email "jdoe@test.de" and "password"
* Check if the sign up was successful

И реализация:


@Step("Sign up as <customer> with email <test@example.com> and <password>")
    public void signUp(String customer, String email, String password) {
        WebDriver webDriver = Driver.webDriver;
        WebElement form = webDriver.findElement(By.id("new_user"));
        form.findElement(By.name("user[username]")).sendKeys(customer);
        form.findElement(By.name("user[email]")).sendKeys(email);
        form.findElement(By.name("user[password]")).sendKeys(password);
        form.findElement(By.name("user[password_confirmation]")).sendKeys(password);
        form.findElement(By.name("commit")).click();
    }

    @Step("Check if the sign up was successful")
    public void checkSignUpSuccessful() {
        WebDriver webDriver = Driver.webDriver;
        WebElement message = webDriver.findElements(By.className("message"));
        assertThat(message.getText(), is("You have been signed up successfully!"));
    }

Полный проект здесь


Плюсы:


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

Минусы:


  • менеджеры не будут читать и писать сценарии
  • приходится следить как за спецификациями, так и за их реализацией (а это и написание кода и правка спецификаций)

Ну и зачем тогда Catcher?


Разумеется, чтобы упростить процесс.


Разработчик пишет только сценарии в json/yaml, а Catcher их выполняет. Сценарий состоит из последовательно выполняемых шагов, например:


steps:
    - http:
        post:
          url: '127.0.0.1/save_data'
          body: {key: '1', data: 'foo'}
    - postgres:
        request:
          conf: 'dbname=test user=test host=localhost password=test'
          query: 'select * from test where id=1'

Catcher поддерживает jinja2 шаблоны, так что можно использовать переменные вместо зашитых значений в примере выше. Глобальные переменные можно хранить в инвентарных файлах (как в ансибле), подтягивать из окружения и регистрировать новые:


variables:
  bonus: 5000
  initial_value: 1000
steps:
- http:
        post:
          url: '{{ user_service }}/sign_up'
          body: {username: 'test_user_{{ RANDOM_INT }}', data: 'stub'}
        register: {user_id: '{{ OUTPUT.uuid }}'
- kafka:
        consume:
            server: '{{ kafka }}'
            topic: '{{ new_users_topic }}'
            where:
                equals: {the: '{{ MESSAGE.uuid }}', is: '{{ user_id }}'}
        register: {balance: '{{ OUTPUT.initial_balance }}'}

Дополнительно можно запускать проверочные шаги:


- check: # check user’s initial balance
    equals: {the: '{{ balance }}', is: '{{ initial_value + bonus }}'}

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


include:
    file: register_user.yaml
    as: sign_up
steps:
    # .... some steps
    - run:
        include: sign_up
    # .... some steps

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


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


#!/bin/bash
one=$(echo ${1} | jq -r '.add.the')
two=$(echo ${1} | jq -r '.add.to')
echo $((${one} + ${two}))

и использование:


---
variables:
  one: 1
  two: 2
steps:
    - math:
        add: {the: '{{ one }}', to: '{{ two }}'}
        register: {sum: '{{ OUTPUT }}'}

Сценарии помещаются в докер файл и запускается через CI.


Также этот образ может быть использован в Marathon/K8s для тестирования существующего окружения. На данный момент я работаю над бэкэндом (аналог AnsibleTower), чтобы сделать процесс тестирования еще проще и удобнее.


Плюсы:


  • не нужно писать код (только в случае кастомных модулей)
  • переключение окружений через инвентарные файлы (как в ансибле)
  • можно использовать собственные модули (на любом языке, даже sh)

Минусы:


  • не человекочитаемый синтаксис (по сравнению с BDD-инструментами)

Вместо заключения


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


Однако инструмент получился гибче, чем я предполагал. Если кого-то заинтересует статья (или сам инструмент), я могу рассказать, как использовать Catcher для организации централизованных миграций и обновления системы микросервисов.


Upd


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


  • end-to-end тесты это не юнит тесты. Я уже ссылался на М.Фаулера в этой статье. Юнит тесты находятся в проекте тестируемого бэкэнда (стандартная директория tests) и запускаются каждый раз при изменении кода на CI. А e2e тесты это отдельный проект, они обычно выполняются дольше, тестируют взаимодествие всех участвующих сервисов и не знают ничего о коде вашего проекта (черный ящик).
  • не стоит использовать Catcher для интеграционных (и ниже) тестов. Это дорого. Гораздо быстрее написать тест на вашем ЯП для текущего бэкэнда. End-to-end тесты вам нужны только если ваша бизнес-логика размазана на 2 и более сервиса.
  • Catcher это тоже BDD. С моей точки зрения, основное преимущество перед Gauge/Cucumber — это готовые модули и легкость их добавления. В идеале пишется только тест. В последней компании я написал все 4 теста на стандартных компонентах, ничего не программируя. Соответственно, требования к квалификации (и цена такого специалиста) будут ниже. Необходимо только знание json/yaml и умение читать спецификации.
  • Для написания Catcher тестов придется изучить Catcher-DSL. Увы, это правда. Я вначале хотел сделать так, чтобы тесты писались сами, прямо с микрофона. Но потом подумал что меня тогда уволят за ненадобностью ;) Как уже было сказано выше — Catcher DSL это стандартный json/yaml и спецификации шагов. Ничего принципиально нового.
  • Вы можете использовать стандартные технологии и написать что-то свое. Однако, мы говорим о микросервисах. Это большое количество разных технологий и ЯП и большое количество команд. И если для ява-команды junit + testcontainers будет очевидным выбором, erlang команда выберет что-то другое. В большой компании при 30+ командах наверху примут решение, что все тесты нужно отдать в новую команду infrastructure/qa. Предствляете, как они обрадуются этому зоопарку?
  • Если у вас 4-5 e2e тестов, то вы можете написать все на любом скриптовом языке и забыть об этом. Однако, если с течением времени логика будет меняться, то через 2-4 года вам придется проводить рефакторинг, разнося непосредственно бизнес-логику тестов и реализацию методов доступа к тестируемым компонентам. Значит в итоге вы напишете свой Catcher, просто не такой гибкий. У меня ушло 4 реализации чтобы это понять ;)




К сожалению, не доступен сервер mySQL