Почему, несмотря на накопленный опыт и современный инструментарий, число флаки‑тестов растёт год от года? Исследование BitRise 2025 года показало, что доля команд, которым приходится сталкиваться с флаки-тестами, выросла с 10% в 2022-м до 26% в 2025-м.
Нестабильные тесты сильно бьют по рабочим процессам: из опроса 1600 человек в 2023 году стало ясно, что флаки-тесты съедают 8% рабочего времени, почти столько же, сколько занимает наладка и поддержка тестовых сред. Но реальный вред флаков гораздо больше: они подрывают доверие к здоровым тестам и ставят под вопрос всю автоматизацию.
Вряд ли количество флаков растёт из-за того, что люди разучились писать атомарные тесты и правильно проектировать архитектуру приложений. Скорее проблема в том, что усложняется среда разработки и тестирования:
Больше этапов в пайплайнах
Более сложные рабочие процессы
Больше сторонних зависимостей
Сложность среды, сторонние зависимости - всё это бьёт в первую очередь по E2E-тестам. Проблема в том, что компенсировать эти источники сложности может быть очень дорого. И именно об этом я хочу поговорить сегодня: насколько дорого обеспечить стабильность E2E-тестов?
1. Частые причины нестабильности
Есть много работ с классификацией источников нестабильности во флаки-тестах; опираясь на них, я пройду по наиболее частым причинам.
Ожидания
Причина львиной доли флаков - проблемы с ожиданиями (подробнее про это тут и тут). Автору теста нужно угадать верхнюю границу, дольше которой тест не будет ждать отклика от браузера. Если дали слишком мало времени - тест упадёт. Если дали слишком много - тест будет медленный. Эта проблема - одна из главных причин, по которым переходят с Selenium на, скажем, Playwright с автоматическими ожиданиями. Сам по себе этот переход может сильно урезать количество флаков.
Использование общих ресурсов
Другая важная причина - использование общих ресурсов. Вот простой пример (по мотивам статьи):
def append_data(data, path, encoding="utf-8"):
"""
Добавляем данные в конец файла
"""
# готовим файл
with open(path, 'a', encoding=encoding) as data_file:
# записываем данные
data_file.write(data)
def test_data_written():
data = "Очень важное сообщение"
file = Path("тестовый_файл.txt")
# выполняем тестируемую функцию
append_data(data, "тестовый_файл.txt")
# проверяем запись
assert file.read_text(encoding="utf-8") == data(все примеры доступны здесь)
Тест test_data_written использует ресурс - файл тестовый_файл.txt. Предположим, к этому файлу есть доступ у других тестов. Если на момент выполнения теста в файле уже есть содержимое, тест будет красным: мы прочитаем не только то, что записали, но и то, что было раньше. Если же файл чистый, тест пройдёт, хотя и функция, и тест никак не изменились: это флак.
Зависимость от порядка выполнения
Эта причина, на самом деле, связана с предшествующей. Если у тестов общие ресурсы и один из них не "убрал за собой", все последующие упадут.
Добавим к нашему примеру ещё один тест, скажем, на запись с другой кодировкой:
def test_utf16_data_written():
data = "Друге важное сообщение"
file = Path("тестовый_файл.txt")
encoding = "utf-16"
# выполняем тестируемую функцию
append_data(data, "тестовый_файл.txt", encoding)
# проверяем запись
assert file.read_text(encoding) == dataЕсли его выполнить в одиночку, он пройдёт успешно, а если выполнить после test_data_written - упадёт, когда попытается прочитать записанный прошлым тестом текст в другой кодировке. Порядок выполнения влияет на результат: это флак.
Параллельное выполнение
Плохо настроенное параллельное выполнение тестов - тоже одна из наиболее частых причин сбоев. Если в нашем примере мы запустим тесты параллельно (например, с помощью pytest-xdist), они тоже упадут, из-за того, что запишут одновременно в один файл тексты с разными кодировками.
Зависимость от внешних систем
Предположим, мы написали функцию, запрашивающую у стороннего сервиса наш IP, и протестировали её:
import ipaddress
import requests
def get_my_ip():
url = "https://api64.ipify.org?format=json"
# отправляем запрос к сервису
response = requests.get(url).json()
# возвращаем только ip
return response["ip"]
def test_get_my_ip_returns_something():
# вызываем проверяемую функцию
ip = get_my_ip()
# вместо ассёрта вызываем функцию, которая упадёт при неправильном ip
ipaddress.ip_address(ip)Запустили тест, он прошёл зелёным. А на следующий день акула погрызла кабель на дне океана.
Мы не контролируем внешнюю систему, и любые изменения в ней могут повлиять на результат тестов - без каких-либо изменений в самих тестах или в тестируемом коде. Опять флак.
2. Избавляемся от общего состояния
В нашем примере в большинстве указанных случаев причиной было общее состояние у тестов. Как от него избавиться? Этому нас давно научили классики: нужно писать изолированные тесты.
Вернёмся в наш пример. Вместо того чтобы вслепую хватать общий файл и записывать в конец наши данные, сделаем так, чтобы каждый тест работал с отдельным, уникальным файлом, который будет создаваться только для этого теста и удаляться после запуска:
import uuid
from pathlib import Path
import pytest
from demo_isolation_cost.test_function import append_data
@pytest.fixture
@profile
def temp_file():
"""
Временный файл, удаляемый после работы теста.
"""
# создаём уникальный путь
# (это можно было сделать родной фикстурой Pytest
# tmp_path, но мы будем измерять время выполнения
# операций с файлами, поэтому важно, чтобы они были
# в нашем коде, а не на стороне Pytest)
path = Path(f"test_{uuid.uuid4().hex[:8]}.txt")
# создаём файл
path.write_text("")
# передаём управление тесту
yield path
# убираемся за тестом
path.unlink(missing_ok=True)
@profile
def test_data_written(temp_file):
data = "Очень важное сообщение"
# выполняем тестируемую функцию
append_data(data, temp_file)
# проверяем, что данные записаны
assert temp_file.read_text(encoding="utf-8") == data
@profile
def test_utf16_data_written(temp_file):
data = "Друге важное сообщение"
encoding = "utf-16"
# выполняем тестируемую функцию
append_data(data, temp_file, encoding)
# проверяем запись
assert temp_file.read_text(encoding) == dataТеперь неважно, сколько раз и в каком порядке запускаются тесты, они не будут мешать друг другу.
Но изменилось и кое-что ещё. Теперь вместо двух обращений к файлу (запись, чтение) каждый тест выполняет четыре обращения (создание, запись, чтение, удаление). Попробуем измерить время, которое занимают эти операции:

Оказывается, наше создание и удаление файла заняло больше времени, чем сам тест.
Конечно, здесь речь идёт о долях миллисекунды, и это не повод экономить. Но это из-за того, что наш пример, во-первых, детский, а, во-вторых, относится к уровню юнит-тестов. Если же мы поднимемся выше по пирамиде тестирования, цена доступа к ресурсам быстро перестаёт быть детской.
3. Цена изоляции
Мартин Фаулер, описывая важность атомарных тестов, писал:
"...я считаю крайне важным сохранять тесты изолированными. Если тесты изолированы должным образом, их можно запускать в любом порядке. Но по мере продвижения к функциональным тестам с более широким операционным охватом поддерживать такую изоляцию становится всё сложнее."
Все уже давно научились настраивать быстрые сюиты изолированных юнит-тестов, которые можно было бы запускать одним щелчком и использовать для проверки каждого пулл-риквеста. Сделать это на уровне E2E гораздо сложнее. Здесь оказывается, что изоляция тестов дорого стоит.
Ресурсы
Google использует при запуске тестов практику «герметичных сред»:
"Чтобы решить эти проблемы, в Google мы сделали ставку на эфемерные герметичные SUT (тестируемые системы) и интегрировали их в нашу CI/CD-инфраструктуру. Сначала мы создали универсальный фреймворк для определения, настройки и запуска SUT.
...
При этом подходе все зависимости теста — это компоненты SUT, одобренные командой, которая владеет соответствующей зависимостью. Так снижается нестабильность, характерная для традиционных общих тестовых зависимостей. Наша инфраструктура запускает эти компоненты в изолированных контейнерах, и, если у вас достаточно аппаратных ресурсов, все они могут быть запущены на одной машине. Это устраняет нестабильность и задержки, возникающие при вызовах через физическую сеть.
...
Так как SUT можно запускать отдельно для каждого теста, мы устраняем проблемы, возникающие из-за того, что несколько тестов выполняются параллельно и записывают одни и те же данные, или из-за того, что предыдущий тест оставил хранилище данных в несогласованном состоянии. Вы всегда можете быть уверены, что каждый тест начинается с предсказуемого, чистого состояния."
Звучит как настоящее волшебство: не нужно ничего вручную убирать, всё окружение теста обнуляется автоматически. Во что обходится это волшебство? Запуск некоторых эвфемерных систем может занимать 10 минут или полчаса, и инженеры Google считают это решение хоть и крайне полезным, но дорогим.
Повторим для себя: Google - гигант в духе киберпанка, рыночная капитализация которого сравнима с ВВП богатой страны - считает это решение дорогим, использует выборочно, и признаёт, что не победил флаки.
Да, этот гигант работает с системами огромной сложности. Но инициализировать базу данных для каждого теста дорого и для простых смертных. Cypress сделал изоляцию тестов поведением по умолчанию, и считает это лучшей практикой - что, безусловно, очень здорово. Но при этом опция testIsolation: false всё равно остаётся, и подразумевается, что она будет использоваться именно для тяжеловесных E2E тестов.
Конечно, это преодолимая проблема, и на то, чтобы сделать изолированный запуск дешёвым, тратятся огромные усилия. И это тоже своя цена: создание хорошей инфраструктуры занимает много времени.
Время на создание инфраструктуры
Перейдём от зарубежного киберпанка к отечественному: Сбер, борясь с нестабильностью тестов, несколько лет назад создал механизм, который генерирует всю иерархию бизнес-сущностей для каждого теста, и удаляет её после завершения теста. Реализация этих генераторов заняла больше года, потому что для обеспечения максимального быстродействия тестов решили генерировать сущности не через API приложения, а через прямые вызовы к базе данных.
Справедливости ради нужно сказать, что:
приложение было на тот момент тяжеловесным и монолитным;
создание генераторов дало много побочных преимуществ - в частности, взглянув на генераторы, которые использует тест, теперь сразу понятно, с какими данными этот тест работает.
Достоверность
Ещё одна цена изоляции - достоверность: фокусируя тесты на отдельных операциях, мы уходим от реальных условий использования. В т.ч. из-за этого классический подход "пирамиды тестов" сейчас подвергают критике. С этим отчасти связано то, что более независимый тест может быть сложнее понять, поскольку он оторван от контекста.
Конечно, это не значит что стоит вернуться к практике двадцатилетней давности, когда писались в основном E2E: надёжность и быстрота с лихвой перевешивают достоверность.
Выводы
Всё это не недостатки, а именно цена. Никто не перестанет из-за этого создавать механизмы изолированного запуска тестов. Но вопрос в том, что создание этих механизмов обходится дорого по ресурсам и по времени, а пока это время идёт, с флаками всё равно нужно что-то делать.
4. Отслеживать дешевле, чем искоренять
К сожалению, чаще всего оказывается так, что правильная инфраструктура для "отлова" флаки стоит гораздо дешевле, чем правильная инфраструктура для их искоренения. Это не выбор или-или - нужно и то, и другое; но пока создаётся вторая, первую стоит уже иметь.
Какие здесь есть инструменты? Вот примерный перечень (не взаимоисключающий):
Перезапуск
Чтобы понять, случайно падение или нет, тест можно перезапустить. Иногда это очень дёшево - для того же Pytest существует [специальный плагин], а в сам Pytest версию по настоянию сообщества добавили опцию --lf, позволяющую запускать только упавшие на прошлом прогоне тесты. Правда, эта опция обеспечивает только локальный перезапуск у себя на машине; реализовать перезапуски масштабно тоже может быть дорого - тому же Сберу для этого пришлось переписать все сообщения об ошибках. Но они всё равно сделали это раньше, чем взялись за генераторы сущностей.
Слежение
TMS обеспечивают сбор аналитики по тестам, и с помощью неё можно определить, какие именно тесты падают случайно. По нынешним временам на этом этапе могут помочь нейросети: существуют (обычно платные) инструменты, обнаруживающие флаки по результатам тестов, а некоторые даже помогают исправлять причины нестабильностей.
Сортировка
Главное, что позволяет сократить вред от флаки - автоматическая сортировка результатов. Самый очевидный здесь вариант - настраиваемые категории в Allure Report, они автоматически сортируют упавшие тесты в т.ч. на основе сообщений об ошибках; если причина нестабильности известна, на неё можно не тратить время - система сама связывает с ней результаты тестов.
Карантин
Наконец, если нестабильный тест нельзя исправить сразу, он уходит в карантин, где ждёт своего часа - в идеале с выделением времени на работу с ним, как с любым техническим долгом.
5. Заключение
Почему, несмотря на накопленный опыт и современный инструментарий, число флаки‑тестов растёт год от года? Причина в том, что создать инфраструктуру, которая бы сделала флаки невозможными, очень, очень дорого - и чем сложнее продукты, тем дороже. Конечно, мы не перестанем из-за этого совершенствовать архитектуру и способы чистого запуска тестов. Но пока мы этим занимаемся, должна уже существовать инфраструктура для слежения, отлова и сортировки флаков.
























