Разбираем на части E2E на реальном примере |
06.11.2024 00:00 |
Автор: Баз Дейкстра (Bas Dijkstra) В последние пару лет я все чаще и чаще говорю о тестировании контрактов – как читая лекции, так и работая с клиентами. Контрактное тестирование обещает снизить зависимость от длинных, медленных и дорогих end-to-end тестов. Как это работает на практике? И в целом, как командам перестать так сильно полагаться на медленные и дорогие E2E-тесты? Примечание: я не говорю, что вам нужно избавиться от всех E2E-тестов, разбив их на небольшие кусочки – но для множества тестов это полезное умственное упражнение. Спасибо Юстасу Лаужадису за дискуссию по этому поводу. В этой статье я хочу разобрать пример E2E-теста для Parabank, фиктивного онлайн-банка, и пошагово разбить этот тест на более маленькие, сфокусированные тесты. Тест концентрируется на подаче заявления о займе через сайт Parabanka – он проверяет, что при определенных входных данных ответ на экране будет соответствовать ожидаемому. Для простоты предположим, что архитектура этой опции состоит из трех отдельных компонентов:
Первые два компонента находятся внутри Парабанка, а обработчик займов – внешний сервис, которым пользуется как Парабанк, так и множество других банковских систем. Предположим также, что для всех компонентов уже написаны и используются тесты (юнит или компонентные), чтобы получить быструю обратную связь об изменениях в их поведении. Выглядит это так:
Шаг 0: пишем E2E-тесты В таких ситуациях я обычно ограничивался рядом E2E-тестов, имитирующих пользователя Парабанка, который авторизуется в системе, заполняет форму займа и отправляет ее с разными входными данными – затем результат на экране верифицируется. Я вижу, что многие команды все еще делают именно это – или они не знают, что можно иначе, или убеждены, что «если мы не проверим, как это работает в пользовательском интерфейсе, то мы не верим в наши тесты или не доверяем им. Такой тест, созданный с помощью Selenium или Playwright, может выглядеть примерно так: [TestCase(10000, 1000, 12345, "Approved")] Охват этого теста можно представить так:
Такие тесты, возможно, кажутся хорошей идеей, но у E2E-тестов есть проблема, с которой вы рано или поздно столкнетесь:
К тому же создание и прогон этих E2E-тестов обычно осуществляются на очень поздних стадиях цикла разработки, так как требуют доступности всех компонентов и сервисов для тестирования – это противоречит желанию многих команд получать быструю обратную связь о результатах разработки. Итак, начнем улучшать этот тест шаг за шагом, разбивая его на небольшие кусочки, которые быстрее пишутся и прогоняются. Шаг 1: отделяем фронтэнд-тесты Наши E2E-тесты в их текущем состоянии проверяют множество вещей одновременно. Обычно это плохая идея, так как усложняет выявление первопричин проблем в случае падения тестов. Начнем разбивать наш тест с того, что отделим «Может ли пользователь увидеть результаты заявления на займ в браузере?» от «Правильно ли обрабатывается заявление на займ, и соответствует ли результат ожиданиям?» Делая такое различие, мы получим вот такой охват тестов:
У нас есть юнит- и компонентные тесты для фронтэнда, где вызовы API имитируются любой любимой вами тест-библиотекой, а вызовы API, управляющие тестами бэкэнда, можно написать через Postman или библиотеки вроде RestAssured.Net. Наши исходные E2E-тесты значительно улучшились, но все еще нужно решить ряд проблем:
А значит, снова надо поработать! Шаг 2: удаляем внешний сервис из уравнения Еще один шаг в верном направлении. Я видел множество команд, заменяющих реальные внешние системы симуляциями (имитаторами или заглушками). Лично я предпочитаю тестировать реальные вещи, если это возможно, и использую имитаторы в основном для того, чтобы тестировать:
Так как обработчик займов – внешняя система, предположим, что для команды разработки ПараБанка имеет смысл заменить его на имитатор – чтобы, например, регулярно тестировать, как реагирует приложение ПараБанка, если обработчик займов зависает, возвращает ошибки или неожиданные ответы. Для использования имитаторов вместо реальных систем и компонентов есть и другие разумные причины, но это не цель сегодняшней статьи. Замена реального обработчика займов на симуляцию приведет к такому охвату наших тестов:
Мы вновь успешно снизили охват тестов, но, к сожалению, решив одну проблему, породили другую: в этом случае мы более не тестируем реальную интеграцию между системой ПараБанка и внешним обработчиком займов. В ловушку потери покрытия интеграций между разными компонентами, разбираясь с паззлом интеграционного тестирования, мы попадаем уже второй раз. Настало время это исправить. Шаг 3: тестирование интеграции между фронтэндом и бэкэндом ПараБанка Чтобы проверить интеграцию между фронтэндом и бэкэндом ПараБанка, можно воспользоваться тестированием контрактов. Одно из крупнейших преимуществ контрактного тестирования по сравнению с другими видами интеграционных тестов – это его асинхронность: и поставщик, и потребитель имеют свои обязательства, но для обмена сообщениями не нужен деплой в общее тест-окружение. Это помогает выполнять интеграционное тестирование на более ранних стадиях процесса разработки – по сути мы можем заняться им, пока еще работаем над ПО в локальном окружении. Перефразируя, контрактное тестирование выносит интеграционные тесты вперед, на стадию юнит-тестирования. В этом случае первым нашим вопросом должен быть «О каком типе контрактного тестирования речь?» Так как и поставщик, и потребитель – внутренние сервисы ПараБанка, и мы хотим глубоко и детально проверить интеграцию фронтэнда и бэкэнда, то разумно будет выбрать контрактное тестирование, ориентированное на потребителя (CDCT). Внедрить CDCT нам помогут инструменты вроде Pact или Spring Cloud Contract. После внедрения CDCT для интеграции фронтэнда и бэкэнда охват тестов станет таким:
Шаг 4: тестирование игтеграции между бэкэндом и сервисом обработки займов Затем перейдем к другой интеграции, пострадавшей от разбивки исходного E2E-теста на более мелкие кусочки. Думаю, вашей первой идеей будет применить CDCT и тут, но с этим есть проблемы, затрудняющие реализацию:
В этом случае двустороннее тестирование контрактов (BDCT) будет более подходящим вариантом. В BDCT и потребитель, и поставщик генерируют свой собственный контракт, а затем их сравнивает независимая третья сторона. В экосистеме Pact это Pactflow. BDCT облегчает нагрузку на поставщика – все, что нужно сделать, это предоставить спецификацию OpenAPI, а значит, внедрять такую интеграцию легче. Дополнительный бонус: с шансами мы сократим количество симулируемых на шаге 2 ситуаций. «Счастливые» сценарии, когда обработчик займов отвечает, как ожидается, будут покрыты контрактом – нам придется имитировать только ситуации, когда обработчик ведет себя не покрытым контрактом образом. К примеру, это задержки ответов и серверные ошибки (HTTP 5xx). Итак, что же мы сделали? Добавив тесты, проверяющие реализацию бэкэнда, мы получили финальную декомпозицию тестов для заявлений на займ:
Нам больше не нужно полагаться на медленные и дорогие E2E-тесты, пересекающие границы команд, отделов и даже компаний. Нас также не слишком волнует (неявное) тестирование реализации обработчика займов – нам исходно не нужно было этим заниматься. Вместо этого мы получили внятный набор тестов, покрывающих реализацию отдельных компонентов нашей системы, а также контрактные тесты, ищущие проблемы интеграции в нынешнем и будущих релизах компонентов. Эти тесты, как правило, будут значительно быстрее, а написать и запускать их можно гораздо раньше, что дает возможность быстрее получать обратную связь о поведении ПО. |