惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Last Watchdog
The Last Watchdog
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
S
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
Recorded Future
Recorded Future
I
Intezer
云风的 BLOG
云风的 BLOG
博客园 - Franky
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
The Hacker News
The Hacker News
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
The Register - Security
The Register - Security
B
Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
The Cloudflare Blog
Webroot Blog
Webroot Blog
W
WeLiveSecurity
H
Heimdal Security Blog
博客园 - 三生石上(FineUI控件)
V
Vulnerabilities – Threatpost
G
Google Developers Blog
O
OpenAI News
V
V2EX
罗磊的独立博客
博客园_首页
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
H
Hacker News: Front Page
博客园 - 叶小钗
T
Tor Project blog
AI
AI

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пять ошибок в работе с ожиданиями в UI‑автотестах, из‑за которых тесты падают через раз
artem · 2026-06-03 · via Все публикации подряд на Хабре

Средний

11 мин

7.7K

Знакомый сюжет в любом проекте с UI‑автотестами. Один и тот же тест на CI ведёт себя по‑разному: вчера прошёл, сегодня упал, завтра снова прошёл. Локально работает всегда. В логах непонятное «Element not interactable», в скриншоте на момент падения элемент вроде на месте.

Команда списывает на «flaky test», добавляет ретрай через JUnit Extension, через месяц добавляет ещё один уровень ретраев на CI. Пайплайн билда растёт с трёх минут до двадцати, потому что ретраи теперь срабатывают на половине прогонов. Доверие к автотестам падает, через полгода менеджмент возвращает ручное тестирование на критичные релизы.

В девяти случаях из десяти источник проблемы не в коде приложения и не в инфраструктуре. Тест просто ждёт не то, не там и не так. UI на современном фронтенде — это асинхронная штука с десятками одновременных операций: запросы к API, ленивая загрузка, анимации, рендеринг через React или Vue, который происходит когда движок решит.

Selenium же выполняется синхронно и не имеет встроенного понимания, когда страница «готова». Эту разницу закрывают ожидания, и именно в них накапливаются ошибки, которые потом превращаются в flaky‑тесты.

Разберём пять самых частых и болезненных ошибок, которые встречаются в работе с ожиданиями. Все примеры на Java + Selenium 4, но логика одинаковая для любой связки.

Thread.sleep вместо явных ожиданий

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

driver.findElement(By.id("buy-now")).click();
Thread.sleep(3000);
WebElement modal = driver.findElement(By.cssSelector(".confirmation-modal"));
modal.findElement(By.cssSelector(".btn-confirm")).click();

Проблем тут сразу несколько.

  • Первая — три секунды это либо много, либо мало, никогда не «как раз». В быстром окружении модалка появится через 200 миллисекунд, и тест зря простаивает 2.8 секунды. На CI‑агенте под нагрузкой та же модалка может появиться через 4–5 секунд, и тест упадёт, потому что элемента ещё нет.

  • Вторая проблема — Thread.sleep ждёт ровно указанное время, независимо от того, что происходит на странице. Если модалка уже видна через полсекунды, тест всё равно простоит три секунды.

  • Третья проблема — Thread.sleep бросает InterruptedException, и его приходится либо обрабатывать try‑catch, либо пробрасывать через throws. И то, и другое засоряет код тестов.

Правильное решение — WebDriverWait с явным условием:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
driver.findElement(By.id("buy-now")).click();
WebElement modal = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".confirmation-modal"))
);
wait.until(ExpectedConditions.elementToBeClickable(
    modal.findElement(By.cssSelector(".btn-confirm"))
)).click();

Тест ждёт ровно столько, сколько нужно. Если модалка появилась через 200 миллисекунд — переходим дальше через 200 миллисекунд. Если за 10 секунд не появилась — падаем с понятным TimeoutException и указанием, какого именно элемента не дождались. Это в разы быстрее по среднему времени прогона и в разы информативнее при разборе падений.

Единственное оправданное применение Thread.sleep — это намеренное ожидание, не связанное с UI. Например, проверить, что после какого‑то действия в течение трёх секунд ничего не произошло. Тогда Thread.sleep уместен, но это редкая ситуация.

Один WebDriverWait на весь проект с большим таймаутом

Антипод предыдущей ошибки. Команда узнаёт про WebDriverWait, кто‑то почитал статью про flaky‑тесты и предложил «давайте поставим таймаут побольше, чтобы тесты не падали». В базовом классе появляется константа DEFAULT_TIMEOUT = 60 и единственный WebDriverWait на 60 секунд, который используется для всех ожиданий в проекте.

Внешне всё стало лучше — flaky‑тесты исчезли.

Но когда в приложении ломается фича, и кнопка перестаёт работать, тест теперь падает не за две секунды, а за минуту. Сюита из 200 тестов, в которой половина падает из‑за регрессии, прогоняется не 20 минут, а полтора часа. Разработчики ждут результаты тестов вместо того, чтобы фиксить баг. Когда асинхронная операция тормозит и реально занимает 30 секунд вместо обычных 2, тест всё равно дождётся и не подсветит проблему производительности.

Правильный подход — разные таймауты для разных типов операций:

public class WaitConfig {
    public static final Duration QUICK = Duration.ofSeconds(2);
    public static final Duration NORMAL = Duration.ofSeconds(5);
    public static final Duration SLOW = Duration.ofSeconds(15);
    public static final Duration VERY_SLOW = Duration.ofSeconds(30);
}

Дальше в коде явно выбирается нужный:

new WebDriverWait(driver, WaitConfig.QUICK)
    .until(ExpectedConditions.elementToBeClickable(buttonLocator));

new WebDriverWait(driver, WaitConfig.SLOW)
    .until(ExpectedConditions.urlContains("/order-confirmation"));

new WebDriverWait(driver, WaitConfig.VERY_SLOW)
    .until(ExpectedConditions.textToBePresentInElementLocated(
        By.cssSelector("[data-test='export-status']"), "Completed"
    ));

Появление кнопки после клика — две секунды максимум, иначе что‑то не так. Редирект на страницу подтверждения — пять‑пятнадцать секунд, потому что может быть пара промежуточных проверок на бэкенде. Завершение экспорта отчёта в Excel — до полуминуты, это реально долгая операция.

Конкретные числа подбираются под проект, но принцип неизменный: «один таймаут на всё» — всегда плохо.

Проверка только presence вместо visibility и clickability

В Selenium есть несколько похожих по названию, но разных по смыслу ExpectedConditions. presenceOfElementLocated отвечает только на один вопрос: есть ли элемент в DOM‑дереве страницы. Видимость, кликабельность, перекрытие другими элементами — это всё не его дело.

Типичный код с этой ошибкой:

wait.until(ExpectedConditions.presenceOfElementLocated(By.id("submit")));
driver.findElement(By.id("submit")).click();

Кнопка submit есть в DOM. Но прямо сейчас она может быть скрыта через display: none, потому что форма ещё не валидирована. Может быть перекрыта модалкой с куки‑баннером, которую тест не успел закрыть. Может находиться за пределами видимой области viewport, и Selenium не сможет на неё кликнуть. Может быть в disabled состоянии, потому что бэкенд ещё не подтвердил доступность операции.

Selenium попытается кликнуть и получит одно из исключений: ElementNotInteractableException, ElementClickInterceptedException, или, что хуже, кликнет «через» элемент в координату, где сейчас совсем не submit, и тест продолжит выполнение с совершенно непредсказуемым результатом.

Правильное использование:

// Перед кликом — проверяем, что кликабельно
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit"))).click();

// Перед чтением текста — проверяем видимость
String price = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector("[data-test='total-price']")
)).getText();

// Перед заполнением поля — кликабельность плюс очистка
WebElement input = wait.until(ExpectedConditions.elementToBeClickable(
    By.cssSelector("[data-test='email-input']")
));
input.clear();
input.sendKeys("user@example.com");
  • elementToBeClickable под капотом проверяет и наличие в DOM, и видимость, и активность, и отсутствие перекрытия. Это то, что нужно перед взаимодействием с элементом в 95% случаев.

  • presenceOfElementLocated имеет смысл, только когда вам действительно достаточно знать, что элемент в DOM, и взаимодействие с ним не планируется. Например, проверка, что hidden input с CSRF‑токеном существует на форме. Или проверка структуры DOM в JavaScript‑тестах. В обычных UI‑тестах его использование почти всегда ошибка.

Игнорирование исчезновения промежуточных элементов

Сценарий типичный для современных SPA. Тест нажимает «Сохранить», на форме появляется спиннер, фронтенд отправляет запрос, через секунду‑две спиннер исчезает и появляется тост‑уведомление «Сохранено». Тест проверяет тост.

saveButton.click();
WebElement toast = wait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector(".toast-success")
));
assertThat(toast.getText()).isEqualTo("Сохранено");

Работает в половине случаев. Во второй половине происходит одно из двух.

  1. Первый вариант — на странице уже был тост от предыдущего действия, который ещё не успел исчезнуть. Тест ловит его, считает успехом, проходит, но реально ничего не проверил. Это особенно болезненно в тестах с несколькими сохранениями подряд: первое прошло, второе сломалось, но тест зелёный, потому что зацепился за тост от первого.

  2. Второй вариант — приложение под нагрузкой отвечает медленнее. Спиннер крутится не одну секунду, а четыре. Тест ждёт тост, но Selenium слишком быстро забирает контекст, пока спиннер ещё активен, и блокирует UI. Тост появляется через секунду после таймаута теста.

Правильная последовательность для асинхронных интерфейсов всегда трёхтактная:

// 1. Действие
saveButton.click();

// 2. Дождаться появления индикатора загрузки
WebDriverWait quickWait = new WebDriverWait(driver, Duration.ofSeconds(2));
quickWait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".spinner")));

// 3. Дождаться его исчезновения
WebDriverWait slowWait = new WebDriverWait(driver, Duration.ofSeconds(15));
slowWait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".spinner")));

// 4. Теперь проверять результат
WebElement toast = slowWait.until(ExpectedConditions.visibilityOfElementLocated(
    By.cssSelector(".toast-success")
));
assertThat(toast.getText()).isEqualTo("Сохранено");

Лишние две строчки кода, которые экономят часы разборок с flaky‑тестами. Принцип универсальный: для любой асинхронной операции в UI должен быть какой‑то индикатор её начала и окончания, и тест должен опираться именно на этот индикатор, а не угадывать состояние по конечному результату.

Если в приложении нет явных индикаторов загрузки (что само по себе UX‑проблема), приходится ждать косвенные признаки: исчезновение disabled атрибута с кнопки, появление новых данных в таблице, изменение URL. Главное — найти какой‑то наблюдаемый признак завершения операции и ждать его.

ExpectedConditions без понимания семантики

В Selenium есть около пятидесяти готовых ExpectedConditions. Большинство автоматизаторов используют пять‑семь. Из оставшихся часть имеет неочевидное поведение, и когда такое условие применяется без понимания, тесты ломаются непредсказуемо.

  • textToBePresentInElement и его варианты проверяют вхождение подстроки, а не равенство. Если ждать textToBePresentInElementLocated(locator, "Сохранено"), условие сработает на тексте «Сохранено», но также сработает на «Не сохранено», «Сохранено с ошибкой», «Сохранено: 5 записей». В тестах с разными типами уведомлений это приводит к ложноположительным проходам.

  • attributeContains — то же самое, ищет подстроку в значении атрибута. Условие attributeContains(locator, "class", "active") сработает не только на классе active, но и на active-state, inactive, activated. На страницах с динамическими классами это даёт случайные совпадения.

  • stalenessOf — условие срабатывает только когда элемент полностью пропал из DOM или был перерисован. Если фронтенд использует виртуальный DOM, как React или Vue, элемент может обновиться без пересоздания, и stalenessOf никогда не сработает. Применять его имеет смысл только когда вы точно знаете, что элемент будет удалён физически — например, при смене страницы или после анимации unmount.

  • elementToBeClickable плюс анимации — отдельная история. Если кнопка анимируется и едет в свою позицию через CSS transition, Selenium считает её кликабельной в момент, когда анимация ещё идёт. Координаты элемента в этот момент меняются между запросом «где ты находишься» и реальным кликом. Результат — Selenium кликает в точку, где кнопка была 50 миллисекунд назад, промахивается, тест ловит непонятную ошибку или вообще тихо продолжает с неправильным состоянием.

Лечится отключением анимаций в начале каждого теста через CSS‑инъекцию:

public void disableAnimations() {
    String script = """
        const style = document.createElement('style');
        style.innerHTML = `
            *, *::before, *::after {
                animation-duration: 0s !important;
                animation-delay: 0s !important;
                transition-duration: 0s !important;
                transition-delay: 0s !important;
            }
        `;
        document.head.appendChild(style);
        """;
    ((JavascriptExecutor) driver).executeScript(script);
}

Эту инъекцию имеет смысл вызывать в @BeforeEach базового тестового класса или в методе открытия страницы. После такого автотесты перестают зависеть от скорости анимаций, и сразу пропадает значительная часть случайных падений.

Что делать со всем этим вместе

Перечисленные ошибки редко встречаются по одной. Обычно в проекте есть все пять сразу: где‑то Thread.sleep, где‑то один большой таймаут, где‑то presence вместо clickable, где‑то нет ожидания исчезновения спиннеров, где‑то flaky из‑за анимаций. Чинить их по очереди — долго и неблагодарно, потому что пока чините одно, оставшиеся четыре продолжают портить статистику.

Рабочая последовательность такая. Сначала пишется новый базовый класс с правильными ожиданиями, фабрикой драйверов с отключёнными анимациями и набором констант для таймаутов. Дальше один тест, который чаще всего падает, переписывается на новый базовый класс. Если на нём ничего не упало за неделю прогонов, переписываются следующие пять тестов. Параллельно убираются все Thread.sleep через регулярку по проекту (Thread\.sleep ищется грепом за минуту, заменить вручную тоже недолго).

Через месяц такой работы количество flaky‑тестов на проекте обычно падает раз в пять. Оставшиеся — это уже реальные проблемы: гонки на бэкенде, нестабильные тестовые данные, сетевые таймауты к сторонним сервисам. Их меньше, и с каждым из них работают точечно, а не «добавим ещё один ретрай».

Итого

Большинство flaky‑тестов в UI‑автоматизации чинятся не ретраями и не увеличением общего таймаута, а пересмотром того, чего именно ждёт тест. Меняем Thread.sleep на WebDriverWait с явными условиями, разделяем таймауты по типам операций: от двух секунд для кликабельности до тридцати для долгих фоновых задач, проверяем clickability перед кликом и visibility перед чтением, дожидаемся не только появления результата, но и исчезновения промежуточных индикаторов загрузки, отключаем анимации через CSS‑инъекцию и читаем документацию ExpectedConditions перед применением неочевидных условий.

После такой ревизии flaky‑тесты полностью не исчезают — остаются настоящие, связанные с гонками данных, сетью и нестабильным окружением. Но их становится мало, и каждый чинится осмысленно.

Хотите прокачать практику QA и лучше разобраться в тестировании сервисов и автотестах? Присмотритесь к бесплатным демо-урокам: их проводят преподаватели‑практики, можно познакомиться с форматом обучения и задать вопросы экспертам.

  • 4 июня в 20:00 — «API под контролем: тестирование сервисов с помощью Postman». Записаться
    Покажем, почему знание API стало обязательным навыком для инженера тестирования и как Postman помогает проверять сервисы осмысленно, а не наугад.

  • 16 июня в 20:00 — «ИИ в автотестах: помощник или угроза?». Записаться
    Поговорим о том, как искусственный интеллект уже меняет работу QA‑инженеров, где он действительно помогает, а где может создать новые риски для качества тестов.

Полный список бесплатных уроков июня смотрите в дайджесте.