Содержание

Введение

Часто можно услышать мнение, что в автоматизации тестирования главное — знание Selenium WebDriver и паттернов типа Page Object, а алгоритмы и структуры данных — это удел «настоящих» backend-разработчиков. Это опасное заблуждение.

Когда ваш фреймворк разрастается до 500+ тестов, а время прогона переваливает за час, вопросы эффективности кода выходят на первый план. Ошибка в алгоритме может превратить 5‑минутный тест в 15‑минутный, а с учётом параллельного запуска — просто обрушить билд‑сервер.

В этой статье мы разберём:

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

  • как считать эту самую скорость (нотация Big O) на пальцах;

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

Автоматизатор как разработчик

Давайте посмотрим правде в глаза: чем мы занимаемся 90% времени?

  • читаем данные из Excel / CSV / БД;

  • ищем элементы в DOM‑дереве;

  • фильтруем списки заказов или пользователей в ответах API;

  • строим сложные XPath или CSS‑селекторы;

  • ждём, пока состояние приложения изменится.

Каждая из этих операций — это работа с коллекциями или графами. Если вы используете for внутри for внутри while, не задумываясь о количестве итераций, рано или поздно это вылезет боком.

Матчасть: как считать скорость (Big O)

СХЕМА BIG O
СХЕМА BIG O

Для автоматизатора достаточно понимать базовую шкалу. Запомните этот список — от самого быстрого к самому медленному:

Сложность

Название

Пример в Java

Что это значит для теста

O(1)

Константная

HashMap.get(key)

Мгновенно. Не зависит от размера данных. Идеал.

O(log n)

Логарифмическая

TreeSet.contains() / бинарный поиск

Очень быстро. Если данных в 1000 раз больше, время вырастет всего в 2‑3 раза.

O(n)

Линейная

ArrayList.contains() (без переопределения equals) или простой for

Растёт линейно. 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) дополнительной памяти) в пользу скорости. В тестовых фреймворках, где данных обычно тысячи, а не миллионы, эта жертва абсолютно оправдана

СРАВНЕНИЕ O(n²) VS O(n)
СРАВНЕНИЕ O(n²) VS 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, кстати, поддерживает такой подход из коробки.)

EXPONENTIAL BACKOFF
EXPONENTIAL BACKOFF

Сортировка и кастомные компараторы

Когда вы получаете данные из БД, они не всегда отсортированы. Если вы сортируете их на клиенте (в Java) для последующего сравнения с эталоном, помните:

Collections.sort(list) — использует TimSort. Сложность O(n log n). Это очень хорошо для большинства задач.

НЕ пишите свою пузырьковую сортировку (Bubble Sort) — это O(n²). В Java она уже есть, не изобретайте велосипед.

Важно для скорости: если вам нужно просто проверить, что список отсортирован, не сортируйте его! Просто пройдитесь по нему один раз (O(n)) и проверьте, что каждый следующий элемент больше предыдущего. Это экономит ресурсы.

Шпаргалка для автоматизатора (памятка)

ШПАРГАЛКА MIND MAP
ШПАРГАЛКА MIND MAP
  • 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 помогает принимать решения ещё на этапе проектирования тестового фреймворка.

Помните: ваш тест может падать не потому, что приложение сломалось, а потому что ваш алгоритм поиска данных умер от усталости.

СПАСИБО ЗА ВНИМАНИЕ
СПАСИБО ЗА ВНИМАНИЕ

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