Содержание
Введение
Часто можно услышать мнение, что в автоматизации тестирования главное — знание Selenium WebDriver и паттернов типа Page Object, а алгоритмы и структуры данных — это удел «настоящих» backend-разработчиков. Это опасное заблуждение.
Когда ваш фреймворк разрастается до 500+ тестов, а время прогона переваливает за час, вопросы эффективности кода выходят на первый план. Ошибка в алгоритме может превратить 5‑минутный тест в 15‑минутный, а с учётом параллельного запуска — просто обрушить билд‑сервер.
В этой статье мы разберём:
какие алгоритмы чаще всего пишут автоматизаторы (и где они теряют скорость);
как считать эту самую скорость (нотация Big O) на пальцах;
как применять формулы к реальным задачам (парсинг, ожидания, поиск элементов, работа с данными).
Автоматизатор как разработчик
Давайте посмотрим правде в глаза: чем мы занимаемся 90% времени?
читаем данные из Excel / CSV / БД;
ищем элементы в DOM‑дереве;
фильтруем списки заказов или пользователей в ответах API;
строим сложные XPath или CSS‑селекторы;
ждём, пока состояние приложения изменится.
Каждая из этих операций — это работа с коллекциями или графами. Если вы используете for внутри for внутри while, не задумываясь о количестве итераций, рано или поздно это вылезет боком.
Матчасть: как считать скорость (Big O)

Для автоматизатора достаточно понимать базовую шкалу. Запомните этот список — от самого быстрого к самому медленному:
Сложность |
Название |
Пример в Java |
Что это значит для теста |
|---|---|---|---|
O(1) |
Константная |
|
Мгновенно. Не зависит от размера данных. Идеал. |
O(log n) |
Логарифмическая |
|
Очень быстро. Если данных в 1000 раз больше, время вырастет всего в 2‑3 раза. |
O(n) |
Линейная |
|
Растёт линейно. 1000 элементов → 1 сек, 2000 → 2 сек. |
O(n²) |
Квадратичная |
Вложенные циклы (for‑for) |
Катастрофа. 100 элементов → 1 сек, 200 элементов → 4 сек, 1000 → 100 сек. |
O(n!) |
Факториальная |
Перебор всех перестановок |
В тестах такое писать нельзя. Смертельно. |
Формула расчёта времени (приблизительно):
T = C * O(n), где C — константа (время на одну операцию). Нам важна не точная цифра, а порядок роста при увеличении входных данных.
Кейс 1: поиск дубликатов или пересечений в двух списках (O(n²) vs O(n))
Плохой код (синдром начинающего)
Допустим, у нас есть список ожидаемых имён пользователей и список фактических с бекенда. Нужно найти, кого не хватает.
List<String> expected = List.of("Alice", "Bob", "John"); List<String> actual = ...; // получили из JSON // ПЛОХО: вложенный цикл for (String exp : expected) { boolean found = false; for (String act : actual) { // если списки по 5000 элементов — это 25 млн итераций! if (exp.equals(act)) { found = true; break; } } if (!found) Assert.fail("Not found: " + exp); }
Скорость: O(n * m). Если n = m = 1000 → 1 000 000 действий.
Хороший код (используем структуры) Всего лишь меняем List на HashSet перед проверкой.
Set<String> actualSet = new HashSet<>(actual); // O(n) — один проход for (String exp : expected) { // O(m) if (!actualSet.contains(exp)) { // O(1) — магия хэш‑кода! Assert.fail("Not found: " + exp); } } // Итоговая сложность: O(n + m)
Результат: при 5000 элементов ускорение в десятки и сотни раз. Даже если вы не чувствуете разницы на 10 элементах — почувствуете на 1000.
Важно: мы жертвуем памятью (O(n) дополнительной памяти) в пользу скорости. В тестовых фреймворках, где данных обычно тысячи, а не миллионы, эта жертва абсолютно оправдана

Кейс 2: как уменьшить стоимость поиска элементов
В автоматизации веба мы часто пишем монструозные XPath вида: //div[@class='main']//table/tbody/tr/td[contains(text(),'Итого')]/../following-sibling::td/span.
Скорость выполнения такого запроса — это задача движка браузера, но мы можем влиять на неё алгоритмически.
Даже идеально написанный XPath не спасёт, если вы заставляете Selenium выполнять один и тот же поиск десятки или сотни раз. Главная стоимость здесь зачастую не сам XPath, а количество обращений к браузеру. Каждый вызов findElement() отправляет команду WebDriver, браузер выполняет поиск в DOM и возвращает найденный элемент обратно. Поэтому уменьшение числа таких вызовов часто даёт больший выигрыш, чем оптимизация самого XPath.
Ошибка: поиск элемента внутри тяжёлого iframe или теневого DOM через цепочку findElement внутри цикла.
// Плохо: каждый раз ищем родителя заново List<WebElement> rows = driver.findElements(By.xpath("//table/tbody/tr")); for (int i = 0; i < rows.size(); i++) { // внутри цикла снова дёргаем Selenium (дорогая операция!) WebElement cell = driver.findElement(By.xpath("//table/tbody/tr[" + i + "]/td[2]")); // работаем с cell }
Скорость: O(n) обращений к браузеру. Каждое обращение — это HTTP/REST‑запрос (очень долго).

Как применить формулу эффективности:
Мы не можем изменить O(n) на O(1), потому что нам нужно обойти все строки. Но мы можем уменьшить константу C.
// Хорошо: один запрос к браузеру за всеми ячейками сразу List<WebElement> cells = driver.findElements(By.xpath("//table/tbody/tr/td[2]")); for (WebElement cell : cells) { // работаем с cell }
Здесь мы сократили количество дорогих I/O‑операций с n до 1. По формуле T = C * n мы кардинально уменьшили C.
Кстати, второй плюс этого подхода — мы избегаем классического StaleElementReferenceException, который часто вылетает при повторном обращении к ранее найденным элементам в динамических таблицах
Кейс 3: ожидания и ретраи (экспонента против константы)
Тема ретраев (повторных попыток) — классика автоматизации. Часто пишут такой цикл ожидания:
// Плохо: линейные попытки с фиксированным шагом for (int i = 0; i < 10; i++) { try { element.click(); break; } catch (Exception e) { Thread.sleep(1000); } }
В худшем случае такой цикл выполнит до 10 попыток с фиксированной задержкой между ними. Если приложение временно недоступно, мы будем снова и снова выполнять одинаковые проверки через равные интервалы.
Алгоритмический подход (Exponential Backoff): Вместо линейного ожидания используем геометрическую прогрессию (увеличиваем интервал с каждым разом).
1‑я попытка: ждём 1 сек
2‑я попытка: ждём 2 сек
3‑я попытка: ждём 4 сек
4‑я попытка: ждём 8 сек
Количество попыток по-прежнему остаётся O(n), но меняется стратегия ожидания. Вместо одинаковых пауз между запросами интервал постепенно увеличивается, что снижает нагрузку на систему и делает ожидание более эффективным. (Awaitility, кстати, поддерживает такой подход из коробки.)

Сортировка и кастомные компараторы
Когда вы получаете данные из БД, они не всегда отсортированы. Если вы сортируете их на клиенте (в Java) для последующего сравнения с эталоном, помните:
Collections.sort(list) — использует TimSort. Сложность O(n log n). Это очень хорошо для большинства задач.
НЕ пишите свою пузырьковую сортировку (Bubble Sort) — это O(n²). В Java она уже есть, не изобретайте велосипед.
Важно для скорости: если вам нужно просто проверить, что список отсортирован, не сортируйте его! Просто пройдитесь по нему один раз (O(n)) и проверьте, что каждый следующий элемент больше предыдущего. Это экономит ресурсы.
Шпаргалка для автоматизатора (памятка)

contains()уArrayList— это O(n). Если вы вызываетеlist.containsв цикле — заменяйте наHashSet.String.concat()или += в цикле — это O(n²). ИспользуйтеStringBuilder(O(n)).Регулярные выражения — мощный инструмент, но сложность их работы часто экспоненциальная (катастрофический бэктрекинг). Если парсите большие HTML‑ответы — лучше используйте JSoup (работает за линейное время), а не крафтовый regex.
Ранний выход (
break). Если нашли нужный элемент — сразу прерывайте цикл. Средняя сложность поиска в несортированном списке — O(n/2), что лучше, чем O(n), если мы идём до конца.Не обманывайтесь красивым синтаксисом
Stream API.list.stream().filter(actual::contains)— это всё тот же вложенный цикл, если не перевести actual в HashSet перед стримом
Вывод
Алгоритмы в автоматизации — это не про «написать сложно», а про «написать эффективно». Знание нотации Big O помогает принимать решения ещё на этапе проектирования тестового фреймворка.
Помните: ваш тест может падать не потому, что приложение сломалось, а потому что ваш алгоритм поиска данных умер от усталости.
