
В первой части мы озвучили следующие тезисы:
автотесты нужны не для экономии на тестировщиков, а чтобы сократить до минимума циклы разработки и узнавать об ошибках практически мгновенно;
покрытие обязано быть абсолютным — должно быть протестировано буквально все, что возможно протестировать: функционал, нагрузку, интерфейс, безопасность, миграции и тому подобное;
тесты ломают разработчики, поэтому им за них отвечать — все виды тестов должны писать и поддерживать разработчики;
с полным автоматическим регрессом можно и нужно ставиться в прод после каждого изменения в кодовой базе;
главный шаблон поставки в прод изменений — конвейер развертывания (Deployment Pipeline);
конвейер делится на 2 главные фазы: commit stage и acceptance stage;
первая фаза — быстрые тесты (до 5 минут), чтобы быстро узнать, что мастер сломан, и его надо скорее чинить;
вторая фаза — приёмочные тесты (до 1 часа), чтобы узнать, можно ли ставить в прод изменения.
Про быстрые тесты мы поговорили во второй части. Пришло время поговорить про короля автотестов - приёмочное тестирование.
Приёмочные тесты
Почему король автотестов? Потому что главное в тестах - проверить, что изменения можно отдать в прод. Поэтому самая важная часть релизного цикла - приёмочное тестирование. А самая важная часть конвейера развертывания - Acceptance-stage с теми самыми приемочными автотестами.
Термин "приёмочные тесты", он же Acceptance Testing или просто acceptance, происходит от идеи приемо-сдаточных испытаний (ПСИ). То есть смысл этих тестов - приемка изменений перед передачей в прод.
Здесь нам нельзя облажаться, поэтому тут должно быть почти все, как в проде: приложение поднято, подключено ко всем тем же технологиям, развернуто теми же скриптами, что и в проде. На этих тестах мы должны проверить весь функционал, UI, нагрузку, разные инфраструктурные отказы, безопасность. Набор кейсов должен быть полный, без дыр.
Функциональные приёмочные тесты
Приёмочные функциональные тесты - король королей. Их больше всех, и, в отличие от других видов тестов, эти нужны всегда. Не важно, огромный ли у вас highload-enterprise или небольшой софт для внутреннего пользования на двух с половиной пользователей и с минимумом данных.
Основная задача функциональных тестов: проверка бизнес-логики приложения. Тесты гонятся против запущенного приложения через его API: HTTP-эндпоинты, очереди/топики, CLI и иные внешние каналы.
Типовые опасности и ловушки при таком автотестировании.
-
Функциональные тесты через UI
Тесты через пользовательский интерфейс с помощью инструментов типа Selenium - распространённая ошибка. На самом деле, это вроде бы очень логично тестировать приложение именно так, как оно будет использоваться - через интерфейс. Но тут есть две проблемы: скорость UI-тестов и привязка интерфейса к тестам.
И самое страшное тут именно второе. Из-за того, что функциональных тестов во взрослой системе обычно несколько сотен (а порой даже тысяч), то любое изменение интерфейса будет требовать переписывания огромного количества тестов. В результате, правки UI станут слишком дорогим, и вы окажетесь в положении замёрзшего навсегда интерфейса.
Поэтому функционал тестируем отдельно, интерфейс - отдельно. Абстрагируем бизнес-логику от UI. В идеале, приёмочные тесты должны быть написаны так, чтобы верхнеуровнево вообще непонятно было, как именно выполнен сценарий: через UI, через API или ещё как-то.
Например, сценарий: войти в магазин, найти книгу Х, положить книгу в корзину и оплатить карточкой. Он может быть выполнен через интерфейс в браузере, через мобильное приложение, через API или вообще вживую в настоящем магазине.
-
Привязка к базе данных
Таким очень часто грешат автотестеры, но и разрабы не отстают: вызовут какой-нибудь POST-метод и на валидации делают селект в базу, чтобы проверить, что ресурс сохранился. В целом, тесты свою роль выполняют, но таким образом вы привязываете свои тесты к структуре базы данных.
Правильный подход - тестирование приложения желательно проводить только через его внешние интерфейсы. Исключение: взаимодействия с внешними системами, где валидации вызова не обойтись.
Как и с интерфейсными тестами, вы просто рискуете при не очень больших изменениях структуры в какой-нибудь центральной сущности наткнуться на необходимость исправления доброй половины всех ваших тестов. И в следующий раз вы крепко подумаете перед тем, как полезете менять что-то в базе. А такого быть не должно: тесты именно для того и нужны, чтобы вы не боялись проводить большие изменения даже в самых ответственных участках.
Важно: тесты нужны не просто для проверки приложения, а для проверки изменений. Поэтому пишите тесты так, чтобы они не добавляли проблем при изменении приложения, а наоборот облегчали его.
Впрочем, оговорюсь, я знаю очень сильную команду, которая тестирует через привязку к базе, и не сильно от этого страдает. Так что, выбор, как обычно, за вами.
-
Поход в API других команд
Тоже распространенная проблема. О ней мы говорили еще в первой части. Хотите, чтобы половину времени ваши тесты были красными, и вы не знали бы, почему? Разрешите вашему приложению ходить в API других команд. Тогда каждый раз, когда у них будет что-то меняться, ломаться или моргать инфра, ваши тесты будут красными, хотя в вашем коде никаких ошибок не будет.
Закройте все внешние зависимости моками и заглушками: используйте Wiremock, подключите приложение к тестовым очередям.
Конечно, дело тут не только в поломанных тестах. Заглушки позволят вам разрабатывать функционал, даже когда ваши соседи ещё не готовы со своим. Также заглушки позволяют протестировать разные ответы, в том числе проверить сценарии недоступности.
-
Неизолированные тесты
Мало, что бесит так, как флакающие тесты, которые от прогона к прогону то красные, то зелёные. У такого поведения бывают разные причины: гонки, нестабильная инфраструктура, использование рандомайзера для тестовых данных и неизолированные тесты.
Неизолированные тесты - это тесты, которые влияют друг на друга при прогоне. Обычно, это происходит из-за того, что в одном тесте создаются данные, которые меняют логику следующего теста. Из-за случайного порядка выполнения, это приводит к случайному поведению тестов. Лечится сбросом состояния приложения между тестами.
UI-тесты
Если в вашей системе есть интерфейс, то вряд ли вы хотели бы отдавать изменения в нём без тестирования. Самая главная мысль тут такая: через UI-тесты надо тестировать только то, что относится к интерфейсу. Никакой бизнес-логики - только user journey и всякие отказы.
Здесь есть 2 основных шаблона:
-
UI-тесты через специальные инструменты типа Selenium или Cypress
Этот вариант позволяет протыкивать все варианты взаимодействия с фронтом через браузер.
Преимущество: тестируется фронт от края до края.
Недостаток: тесты медленные, сетап нетривиальный.
Мы в своей команде тестируем фронт через Selenium вместе с реальным приложением. Это вызывает некоторые трудности с тестированием отображения специфических ошибок на серверной стороне, но, в целом, этого механизма нам более чем достаточно.
В своё время мы работали с Cypress, но мы пишем бэк, как и приёмочные тесты, на джаве, поэтому и перешли на селениум: так мы можем использовать код приёмочных тестов для подготовки данных под UI.
-
Humble Object
Другой вариант тестирования фронта - шаблон Humble Object. Это подход, когда вся сложная логика вытаскивается из элементов, которые тяжело протестировать, оставляя их настолько примитивными, что особой нужды писать на них тесты и нет. Оставшаяся часть фронта тестируется стандартными тестовыми инструментами языка программирования.
Явное преимущество такого способа тестирования - скорость тестов. Если на Селениуме сотня тестов будет идти минут пять-десять, то на каком-нибудь Джесте - несколько секунд.
Однако, во-первых, это требует соответствующей архитектуры приложения. Во-вторых, все же это не полные тесты.
Но мы это подход не пробовали, так что не берусь всерьез критиковать. Может быть, указанные недостатки и надуманы. Может, в комбинации с Селениумом они обретут свою полноту или, кто знает, полнота эта и не нужна совсем. А архитектуру недостатком называть вообще странно.
Нагрузочные тесты
Многие приложения в процессе работы сталкиваются с такой нагрузкой, что не учитывать при разработке это уже нельзя.
Например:
необходимо отрабатывать большой поток запросов;
необходимо обрабатывать очень большой массив данных за ограниченное время;
необходимо возвращать результат поиска по большому количеству данных за ограниченное время;
Иногда проблема не в нагрузке, а в строгих требованиях:
необходимо упихаться в очень небольшие ресурсы;
необходимо уложить обработку в очень небольшое время.
И, если вы пообщаетесь с профильными нагрузочными тестировщиками, то они вам обязательно расскажут, что нужно мерить профиль нагрузки, подавать нагрузку постепенно и измерять порог деградации. То есть проводить полноценное исследование каждого релизного артефакта.
Да. конечно, это не вредно, но на этом вы за полчаса в прод не уедете. А на чём уедете?
-
Приёмочные нагрузочные тесты
Если есть возможность повторить на тестовом стенде вашу инфраструктуру из прода, то можно просто транслировать требования к системе в приёмочный тест.
Например, если система должна держать 1k RPS, то можно подать 10k запросов и проверить, что все обработалось и время обработки не превысило 10 секунд.
-
Экстраполяция нагрузки
Если возможности иметь такой же стенд нет, то можно интерполировать результаты на более скромном стенде. Например, вы должны запихнуть обработку миллиона сущностей в 16 Гб ОЗУ. На стенде вам дают лишь 2 гигабайта. Ну и тестируйте на ~100 тысячах сущностей, только удостоверьтесь, что это корректная экстраполяция и зелёный тест значит, что в проде все ок, а красный - что не ок. Если экстраполяция верна, то, когда тест свалится по памяти, вы сможете поймать деградацию.
-
Нагрузочные юнит-тесты
В некоторых ситуациях нам приходится ловить n². И на небольших объёмах вы эту проблему не отловите. А приёмочный тест на больших объёмах может длиться очень долго.
В таком случае я рекомендую использовать нагрузочные юнит-тесты. Это позволит вам тестировать нагрузку в десятки тысяч сущностей в доли секунды, что идеально подходит для таких задач. Если вы все же вляпались в n², вы очень быстро увидите, что ваш тест вместо одной секунды едет десятки секунд, падая по таймауту.
Такие тесты - очень удобный инструмент оптимизации.
Техника безопасности при нагрузочном тестировании
С нагрузочным тестированием надо быть очень аккуратным: то, что работает на стенде совершенно не факт, что будет работать в проде. Вас могут ввести в заблуждение различия в тестовых данных, в сетевой инфраструктуре, нагрузке на сеть и железо от других приложений и ещё во многих нюансах. То есть ваши тесты могут не отлавливать реальный регресс по самым разным причинам.
Поэтому советую пытаться валидировать все ваши новые нагрузочные тесты, сравнивая их с работой приложения в проде. Для этого очень желательно предусматривать при поставках плавную подачу нагрузки на приложение, чтобы вовремя отловить деградацию. И хорошенько продумывать разные фейловеры, чтобы не угодить впросак, когда вы всё-таки не угадали с тестом, а это рано или поздно случится.
Вообще совет валидировать новые тесты на корректность - актуален для всех видов тестов.
Вдобавок, крайне рекомендую на тестовом стенде иметь сопоставимое с продом по объёму хранилище, чтобы ещё на тестах видеть проблемы с изменением схемы данных и неэффективные запросы.
В общем, нагрузка и нагрузочное тестирование - это страшно скользкая тема, с которой надо быть очень аккуратным.
Infrastructure as code
Может ли некорректное изменение инфраструктурных скриптов сломать приложение в проде? Легко!
Что это значит? Это значит, что любое изменение инфраструктуры обязано проходить абсолютно такое же тестирование, что и весь остальной код.
Эта практика называется Infrastructure as Code. Все, что касается инфраструктуры должно восприниматься так же, как и весь остальной код. Инфраструктурный код должен храниться в одном репозитории с исходным кодом приложения и тестами, а каждое изменение инфраструктурного кода должно также подвергаться приёмочному тестированию.
Ну и для валидности тестирования, очевидно, что в проде и на стенде должны работать одни и те же скрипты. А то получается, что тестируете вы одно, а ставите другим.
Техника безопасности при приёмочном тестировании
Как обычно, завершаем гайд техникой безопасности - небольшим набором напутствий.
Приёмочные тесты - инструмент валидации изменений перед постановкой в прод. Если у вас нет настоящего пайпа, ваши релизные циклы длятся неделями, а перед постановкой в прод вас начальники обязывают проводить ручной регресс, то теоретически вы можете сэкономить на этих тестах и ограничиться юнит-тестами. Но из любви к искусству можно и не экономить - польза от них все равно есть и немалая.
Приёмочные тесты не должны ехать больше получаса. Час - край. Классический приём с ночными джобами часто приводит к покрасневшему на недели пайпу, потому что долгая обратная связь не позволяет быстро понять, почему сломался какой-то тест. А когда вы починили один, то уже сломался другой.
Приёмочное тестирование требует пайплайна, стендов, автоматизации развертывания, распараллеливания. Ваши девопсы с такими хотелками вас могут послать, потому что они "знают лучше", а это значит, что вам придётся делать все самим. Если вам позволят. И вот это "если" - это вполне себе реальный риск, с которым я, например, к своему сожалению, сталкивался.
Приёмочное тестирование требует соответствующих вашим тестам стендов. Если тестов много, то нужно несколько стендов, чтобы их распараллелить. Если есть нагрузочное тестирование, то потребуется ну хоть хоть сколько-нибудь производительный стенд и объем данных на уровне того, что у вас в проде. Этого вам могут не дать.
Приёмочные тесты должны писать разработчики, в противном случае ничего не выйдет
Если вы хорошо оптимизировали фазу приёмочного тестирования, и не очень страдаете от того, что у вас нет быстрых тестов, то вы можете плюнуть на юнит-тесты и ограничиться только приёмочными тестами. Но учтите, если вам нужен регулярный рефакторинг, и вы умеете делать его маленькими шагами, то лучше на юнитах не экономить.
Если у вас есть нагрузка, то очень трудно придумать что-то лучше приёмочных тестов. Не экономьте на нагрузочных acceptance-тестах, иначе вы рискуете начать ронять прод серийно, раз за разом откатывая свои релизы. Замучаетесь исправлять последствия аварий, а все сроки поедут, потому что новый функционал будет все время ждать, пока вы не исправите нагрузку.
Меня зовут Саша Раковский. Работаю техлидом в расчетном центре одного из крупнейших банков РФ, где ежедневно проводятся миллионы платежей, а ошибка может стоить банку очень дорого. Законченный фанат экстремального программирования, а значит и DDD, TDD, и вот этого всего. Штуки редкие, крутые, так мало кто умеет, для этого я здесь - делюсь опытом. Если стало интересно, добро пожаловать в мой блог.
PaulIsh
Имеется в виду какая-то рабочая база данных? Если вообще ни к какой БД не привязываться, то можно поломать слой сохранения/считывания данных и в тестах это не увидеть. Или вы имеете в виду, что для тестов надо всегда разворачивать какую-то базу с нуля?
RakovskyAlexander Автор
Да, перечитал и понял, что недостаточно понятно объяснил. Не стоит валидацию в тесте привязывать к базе. Конечно, приложение на стенде должно ходить в тестовый инстанс базы, но имелось в виду, что все взаимодействие с приложением должно происходить через публичные интерфейсы приложения.
Как неправильно:
Создали через апи сущность
Сделали селект и увидели, что сущность создана
Как правильно:
Создали через апи сущность
Запросили сущности через апи
Проверили, что созданная вернулась.