Полный гайд по автотестам для лидов и разработчиков. Часть 3. Про царь-тесты

В первой части мы озвучили следующие тезисы:
-
автотесты нужны не для экономии на тестировщиков, а чтобы сократить до минимума циклы разработки и узнавать об ошибках практически мгновенно;
-
покрытие обязано быть абсолютным — должно быть протестировано буквально все, что возможно протестировать: функционал, нагрузку, интерфейс, безопасность, миграции и т.п.;
-
тесты ломают разработчики, поэтому им за них отвечать — все виды тестов должны писать и поддерживать разработчики;
-
с полным автоматическим регрессом можно и нужно ставиться в прод после каждого изменения в кодовой базе;
-
главный шаблон поставки в прод изменений — конвейер развертывания (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, через 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. Все, что касается инфраструктуры должно восприниматься так же, как и весь остальной код. Инфраструктурный код должен храниться в одном репозитории с исходным кодом приложения и тестами, а каждое изменение инфраструктурного кода должно также подвергаться приёмочному тестированию.
Ну и для валидности тестирования, очевидно, что в проде и на стенде должны работать одни и те же скрипты. А то получается, что тестируете вы одно, а ставите другим.
Техника безопасности при приёмочном тестировании
Как обычно, завершаем гайд техникой безопасности — небольшим набором напутствий.
-
Приёмочные тесты — инструмент валидации изменений перед постановкой в прод. Если у вас нет настоящего пайпа, ваши релизные циклы длятся неделями, а перед постановкой в прод вас начальники обязывают проводить ручной регресс, то теоретически вы можете сэкономить на этих тестах и ограничиться юнит-тестами. Но из любви к искусству можно и не экономить — польза от них все равно есть и немалая.
-
Приёмочные тесты не должны ехать больше получаса. Час — край. Классический приём с ночными джобами часто приводит к покрасневшему на недели пайпу, потому что долгая обратная связь не позволяет быстро понять, почему сломался какой-то тест. А когда вы починили один, то уже сломался другой.
-
Приёмочное тестирование требует пайплайна, стендов, автоматизации развертывания, распараллеливания. Ваши девопсы с такими хотелками вас могут послать, потому они «знают лучше», а это значит, что вам придётся делать все самим. Если вам позволят. И вот это «если» — это вполне себе реальный риск, с которым я, например, к своему сожалению сталкивался.
-
Приёмочное тестирование требует соответствующих вашим тестам стендов. Если тестов много, то нужно несколько стендов, чтобы их распараллелить. Если есть нагрузочное тестирование, то потребуется ну хоть хоть сколько-нибудь производительный стенд и объем памяти на уровне того, что у вас в проде. Этого вам могут не дать.
-
Приёмочные тесты должны писать разработчики, в противном случае ничего не выйдет
-
Если вы хорошо оптимизировали фазу приёмочного тестирования, и не очень страдаете от того, что у вас нет быстрых тестов, то вы можете плюнуть на юнит-тесты и ограничиться только приёмочными тестами.
-
Если у вас есть нагрузка, то очень трудно придумать что-то лучше приёмочных тестов. Не экономьте на нагрузочных accpetance-тестах, иначе вы рискуете начать ронять прод серийно, раз за разом откатывая свои релизы. Замучаетесь исправлять последствия аварий, а все сроки поедут, потому что весь функционал будете откатывать обратно.
Меня зовут Саша Раковский. Работаю техлидом в расчетном центре одного из крупнейших банков РФ, где ежедневно проводятся миллионы платежей, а ошибка может стоить банку очень дорого. Законченный фанат экстремального программирования, а значит и DDD, TDD, и вот этого всего. Штуки редкие, крутые, так мало кто умеет, для этого я здесь — делюсь опытом. Если стало интересно, добро пожаловать в мой блог.
Автор: RakovskyAlexander

