В первой части мы озвучили следующие тезисы:

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

  • покрытие обязано быть абсолютным — должно быть протестировано буквально все, что возможно протестировать: функционал, нагрузку, интерфейс, безопасность, миграции и тому подобное;

  • тесты ломают разработчики, поэтому им за них отвечать — все виды тестов должны писать и поддерживать разработчики;

  • с полным автоматическим регрессом можно и нужно ставиться в прод после каждого изменения в кодовой базе;

  • главный шаблон поставки в прод изменений — конвейер развертывания (Deployment Pipeline);

  • конвейер делится на 2 главные фазы: commit stage и acceptance stage;

  • первая фаза — быстрые тесты (до 5 минут), чтобы быстро узнать, что мастер сломан, и его надо скорее чинить;

  • вторая фаза — приёмочные тесты (до 1 часа), чтобы узнать, можно ли ставить в прод изменения.

Про быстрые тесты мы поговорили во второй части. Пришло время поговорить про короля автотестов - приёмочное тестирование.

Приёмочные тесты

Почему король автотестов? Потому что главное в тестах - проверить, что изменения можно отдать в прод. Поэтому самая важная часть релизного цикла - приёмочное тестирование. А самая важная часть конвейера развертывания - Acceptance-stage с теми самыми приемочными автотестами.

Термин "приёмочные тесты", он же Acceptance Testing или просто acceptance, происходит от идеи приемо-сдаточных испытаний (ПСИ). То есть смысл этих тестов - приемка изменений перед передачей в прод.

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

Функциональные приёмочные тесты

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

Основная задача функциональных тестов: проверка бизнес-логики приложения. Тесты гонятся против запущенного приложения через его API: HTTP-эндпоинты, очереди/топики, CLI и иные внешние каналы.

Типовые опасности и ловушки при таком автотестировании.

  1. Функциональные тесты через UI

    Тесты через пользовательский интерфейс с помощью инструментов типа Selenium - распространённая ошибка. На самом деле, это вроде бы очень логично тестировать приложение именно так, как оно будет использоваться - через интерфейс. Но тут есть две проблемы: скорость UI-тестов и привязка интерфейса к тестам. 

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

    Поэтому функционал тестируем отдельно, интерфейс - отдельно. Абстрагируем бизнес-логику от UI. В идеале, приёмочные тесты должны быть написаны так, чтобы верхнеуровнево вообще непонятно было, как именно выполнен сценарий: через UI, через API или ещё как-то.  

    Например,  сценарий: войти в магазин, найти книгу Х, положить книгу в корзину и оплатить карточкой. Он может быть выполнен через интерфейс в браузере, через мобильное приложение, через API или вообще вживую в настоящем магазине.

  2. Привязка к базе данных 

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

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

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

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

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

  3. Поход в API других команд

    Тоже распространенная проблема. О ней мы говорили еще в первой части. Хотите, чтобы половину времени ваши тесты были красными, и вы не знали бы, почему? Разрешите вашему приложению ходить в API других команд. Тогда каждый раз, когда у них будет что-то меняться, ломаться или моргать инфра, ваши тесты будут красными, хотя в вашем коде никаких ошибок не будет.

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

    Конечно, дело тут не только в поломанных тестах. Заглушки позволят вам разрабатывать функционал, даже когда ваши соседи ещё не готовы со своим. Также заглушки позволяют протестировать разные ответы, в том числе проверить сценарии недоступности.

  4. Неизолированные тесты

    Мало, что бесит так, как флакающие тесты, которые от прогона к прогону то красные, то зелёные. У такого поведения бывают разные причины: гонки, нестабильная инфраструктура, использование рандомайзера для тестовых данных и неизолированные тесты

    Неизолированные тесты - это тесты, которые влияют друг на друга при прогоне. Обычно, это происходит из-за того, что в одном тесте создаются данные, которые меняют логику следующего теста. Из-за случайного порядка выполнения, это приводит к случайному поведению тестов. Лечится сбросом состояния приложения между тестами.

UI-тесты

Если в вашей системе есть интерфейс, то вряд ли вы хотели бы отдавать изменения в нём без тестирования. Самая главная мысль тут такая: через UI-тесты надо тестировать только то, что относится к интерфейсу. Никакой бизнес-логики - только user journey и всякие отказы.

Здесь есть 2 основных шаблона: 

  1. UI-тесты через специальные инструменты типа Selenium или Cypress

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

    Преимущество: тестируется фронт от края до края.

    Недостаток: тесты медленные, сетап нетривиальный.

    Мы в своей команде тестируем фронт через Selenium вместе с реальным приложением. Это вызывает некоторые трудности с тестированием отображения специфических ошибок на серверной стороне, но, в целом, этого механизма нам более чем достаточно.

    В своё время мы работали с Cypress, но мы пишем бэк, как и приёмочные тесты, на джаве, поэтому и перешли на селениум: так мы можем использовать код приёмочных тестов для подготовки данных под UI.

  2. Humble Object

    Другой вариант тестирования фронта - шаблон Humble Object. Это подход, когда вся сложная логика вытаскивается из элементов, которые тяжело протестировать, оставляя их настолько примитивными, что особой нужды писать на них тесты и нет. Оставшаяся часть фронта тестируется стандартными тестовыми инструментами языка программирования.

    Явное преимущество такого способа тестирования - скорость тестов. Если на Селениуме сотня тестов будет идти минут пять-десять, то на каком-нибудь Джесте - несколько секунд.

    Однако, во-первых, это требует соответствующей архитектуры приложения.  Во-вторых, все же это не полные тесты.

    Но мы это подход не пробовали, так что не берусь всерьез критиковать. Может быть, указанные недостатки и надуманы. Может, в комбинации с Селениумом они обретут свою полноту или, кто знает, полнота эта и не нужна совсем. А архитектуру недостатком называть вообще странно.

Нагрузочные тесты

Многие приложения в процессе работы сталкиваются с такой нагрузкой, что не учитывать при разработке это уже нельзя.

Например:

  • необходимо отрабатывать большой поток запросов;

  • необходимо обрабатывать очень большой массив данных за ограниченное время;

  • необходимо возвращать результат поиска по большому количеству данных за ограниченное время;

Иногда проблема не в нагрузке, а в строгих требованиях:

  • необходимо упихаться в очень небольшие ресурсы;

  • необходимо уложить обработку в очень небольшое время.

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

Да. конечно, это не вредно, но на этом вы за полчаса в прод не уедете. А на чём уедете?

  1. Приёмочные нагрузочные тесты

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

    Например, если система должна держать 1k RPS, то можно подать 10k запросов и проверить, что все обработалось и время обработки не превысило 10 секунд.

  2. Экстраполяция нагрузки

    Если возможности иметь такой же стенд нет, то можно интерполировать результаты на более скромном стенде. Например, вы должны запихнуть обработку миллиона сущностей в 16 Гб ОЗУ. На стенде вам дают лишь 2 гигабайта. Ну и тестируйте на ~100 тысячах сущностей, только удостоверьтесь, что это корректная экстраполяция и зелёный тест значит, что в проде все ок, а красный - что не ок. Если экстраполяция верна, то, когда тест свалится по памяти, вы сможете поймать деградацию.

  3. Нагрузочные юнит-тесты

    В некоторых ситуациях нам приходится ловить n². И на небольших объёмах вы эту проблему не отловите. А приёмочный тест на больших объёмах может длиться очень долго. 

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

    Такие тесты - очень удобный инструмент оптимизации.

Техника безопасности при нагрузочном тестировании

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

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

Вообще совет валидировать новые тесты на корректность - актуален для всех видов тестов.

Вдобавок, крайне рекомендую на тестовом стенде иметь сопоставимое с продом по объёму хранилище, чтобы ещё на тестах видеть проблемы с изменением схемы данных и неэффективные запросы.

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

Infrastructure as code

Может ли некорректное изменение инфраструктурных скриптов сломать приложение в проде? Легко!

Что это значит? Это значит, что любое изменение инфраструктуры обязано проходить абсолютно такое же тестирование, что и весь остальной код.

Эта практика называется Infrastructure as Code. Все, что касается инфраструктуры должно восприниматься так же, как и весь остальной код. Инфраструктурный код должен храниться в одном репозитории с исходным кодом приложения и тестами, а каждое изменение инфраструктурного кода должно также подвергаться приёмочному тестированию. 

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

Техника безопасности при приёмочном тестировании

Как обычно, завершаем гайд техникой безопасности - небольшим набором напутствий.

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

  2. Приёмочные тесты не должны ехать больше получаса. Час - край. Классический приём с ночными джобами часто приводит к покрасневшему на недели пайпу, потому что долгая обратная связь не позволяет быстро понять, почему сломался какой-то тест. А когда вы починили один, то уже сломался другой.

  3. Приёмочное тестирование требует пайплайна, стендов, автоматизации развертывания, распараллеливания. Ваши девопсы с такими хотелками вас могут послать, потому что они "знают лучше", а это значит, что вам придётся делать все самим. Если вам позволят. И вот это "если" - это вполне себе реальный риск, с которым я, например, к своему сожалению, сталкивался.

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

  5. Приёмочные тесты должны писать разработчики, в противном случае ничего не выйдет

  6. Если вы хорошо оптимизировали фазу приёмочного тестирования, и не очень страдаете от того, что у вас нет быстрых тестов, то вы можете плюнуть на юнит-тесты и ограничиться только приёмочными тестами. Но учтите, если вам нужен регулярный рефакторинг, и вы умеете делать его маленькими шагами, то лучше на юнитах не экономить.

  7. Если у вас есть нагрузка, то очень трудно придумать что-то лучше приёмочных тестов. Не экономьте на нагрузочных acceptance-тестах, иначе вы рискуете начать ронять прод серийно, раз за разом откатывая свои релизы. Замучаетесь исправлять последствия аварий, а все сроки поедут, потому что новый функционал будет все время ждать, пока вы не исправите нагрузку.


Меня зовут Саша Раковский. Работаю техлидом в расчетном центре одного из крупнейших банков РФ, где ежедневно проводятся миллионы платежей, а ошибка может стоить банку очень дорого. Законченный фанат экстремального программирования, а значит и DDDTDD, и вот этого всего. Штуки редкие, крутые, так мало кто умеет, для этого я здесь - делюсь опытом. Если стало интересно, добро пожаловать в мой блог.

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


  1. PaulIsh
    13.10.2025 10:02

    Привязка к базе данных

    Имеется в виду какая-то рабочая база данных? Если вообще ни к какой БД не привязываться, то можно поломать слой сохранения/считывания данных и в тестах это не увидеть. Или вы имеете в виду, что для тестов надо всегда разворачивать какую-то базу с нуля?


    1. RakovskyAlexander Автор
      13.10.2025 10:02

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

      Как неправильно:

      • Создали через апи сущность

      • Сделали селект и увидели, что сущность создана

      Как правильно:

      • Создали через апи сущность

      • Запросили сущности через апи

      • Проверили, что созданная вернулась.