Всем привет! Меня зовут Алексей, в hh.ru я занимаюсь автотестами и их инфраструктурой. hh.ru — довольно большой продукт: 150+ микросервисов и 50 команд разработки. Большинство команд пишут E2E-тесты, и на текущий момент написано уже около 1800 тестовых классов, в которых примерно 8000 аннотаций @Test. Как со всем этим жить и как вообще устроено E2E-тестирование в hh.ru разберемся в сегодняшней статье. Поехали!
Процессы
Наши тесты — регрессионные. Их разработка начинается с идеи о том, что проверка какой-либо функциональности должна быть автоматизирована, и в дальнейшем тестировщик не должен к ней возвращаться. Задачи на разработку тестов появляются разными путями: иногда тесты становятся частью задачи по разработке функциональности продукта, иногда — реакцией на баги. А порой команда просто хочет упростить себе жизнь уже после выхода доработок сайта.
Когда работа над тестом закончена, он проверяется на стабильность: многократно запускается нашим внутренним инструментом — Kraken — при нагруженном тестовом стенде. Если успешных запусков больше 95%, то тест отправляется на ревью, в противном случае его ждут доработки.
Чтобы распределять задачи на ревью, мы пользуемся фичей гитхаба, которая автоматически назначает ответственного за ревью из предсозданного списка, учитывая codeowners — изменение некоторых участков кода требует ревью определённой команды. И уже после ревью тест попадает в мастер.
Процессы и инструменты
Наши тесты — это проект на Java с TestNg, Selenium WebDriver, Kraken. Для запуска мы используем Jenkins, в котором каждому тестовому стенду назначен свой view, выглядит это например так:
Кто, когда и как запускает тесты
Есть несколько возможных инициаторов запуска тестов. Например, тестировщик получает задачу на тестирование, разворачивает её на стенде и запускает тесты. Или разработчик, завершив работу над задачей и поставив соответствующий статус в Jira, триггерит нашу CI/CD-систему, а она разворачивает задачу на стенде и запускает тесты на Jenkins. Аналогичный процесс происходит при тестировании релиза. На каждый день для релизов назначается дежурный инженер. При фейлах тестирования релиза, часть задач дежурного — самостоятельно или при помощи команды-владельца разобраться, почему упал тест, и принять решение: отключить тест, исключить из релиза задачу, которая ломает тест, или исправить тест.
Железо и софт
Наш тестовый стенд — виртуальная машина на Linux с одиннадцатью процессорными ядрами, где работают база данных и несколько инфраструктурных сервисов. Вторая часть стенда — одноимённый namespace в k8s-кластере, где работает большая часть сервисов. У каждого инженера есть свой тестовый стенд.
Есть ферма с браузерами — одна для всех. Это набор виртуальных машин суммарной мощностью 600 процессорных ядер: здесь запускаются chrome-браузеры от наших E2E тестов. В качестве селениум хаба мы используем Selenoid + Ggr от aerocube и постепенно мигрируем в сторону собственного решения, аналога moon/zalenium
О нагрузке и балансировке
Как я уже упоминал в начале статьи, у нас довольно большое количество тестов, кроме того мы тестируем и выпускаем несколько релизов каждый день. Разумеется, наши тесты работают в параллель, но неясно, сколько именно надо запускать тестов, чтобы:
Тесты прошли максимально быстро
Это не приводило к нестабильности тестов
Чтобы решить эти вопросы, мы написали свой инструмент — Kraken. Его основные задачи — программно создавать экземпляры TestNg, и в каждом из них запускать один тестовый класс. В зависимости от текущего потребления CPU на стенде, он также должен менять количество слотов для экземпляров TestNg. Так мы получили возможность использовать всю доступную мощь стенда, но при этом не увеличивать количество false negative результатов. Тестов всегда работает столько, сколько в состоянии выдержать стенд.
Вот наш незамысловатый алгоритм балансировки:
private Integer calculateFreeThreads(Integer freeThreads, Collection<SuiteInstance> runningSuites) {
LinkedList<Pair<LocalDateTime, Float>> all = LOAD_HISTORY.getCopy();
if (all.isEmpty()) {
return freeThreads > 0 ? freeThreads : 1;
}
if (lastInfoRecord == null) {
lastInfoRecord = all.getLast().getLeft();
}
if (LocalDateTime.now().minusSeconds(30).isAfter(all.getLast().getLeft())) {
throw new RuntimeException("30 seconds cant get time from stand, some wrong");
}
if (!all.getLast().getLeft().isAfter(lastInfoRecord)) {
return freeThreads;
}
lastInfoRecord = all.getLast().getLeft();
if (runningSuites.size() > maxRunningTests) {
return registerChangeAndReturn(0);
}
if (lastChangeTime.isBefore(LocalDateTime.now().minusSeconds(1 * multiplier.get()))) {
double average5 = calculateAverageOrZero(all);
if (average5 > maxLoad) {
return registerChangeAndReturn(0);
}
if (all.getLast().getValue() > maxLoad && average5 < maxLoad) {
return registerChangeAndReturn(freeThreads > 1 ? 1 : freeThreads);
}
if (all.getLast().getValue() > maxLoad) {
return registerChangeAndReturn(0);
}
if (all.getLast().getValue() < maxLoad && average5 < maxLoad) {
return registerChangeAndReturn(++freeThreads);
}
}
return freeThreads;
}
Однако некоторое количество нестабильных тестов всё равно присутствует. Обычно из 8000 с первого раза не проходят 30-50 методов, и здесь мы подстелили-таки соломку. В случае падения теста Kraken автоматически запустит его ещё раз, а если и вторая попытка не увенчается успехом, тест будет помечен как проваленный. По результатам запуска из проваленных тестов будет автоматически создана job на Jenkins, которую можно запустить после разбора инцидентов.
Тесты и фреймворк
Наши тесты — это проект на Java, где мы стараемся максимально разделить фреймворк и сами тесты. Простое API помогает быстро освоиться и начать писать тесты разработчикам на других языках или инженерам, мало знакомым с программированием. Вся работа с браузером скрыта от пользователя в PageObject, для подавляющего большинства страниц классы уже существуют. Повторяющиеся блоки сайта, например, различные меню, отображены в свои классы. Реализован принцип “один поток — один тест — один браузер”. C помощью веб-драйвера мы также отлавливаем ошибки JavaScript.
Вот так выглядит типичный тест:
@Labels(list = {EMPLOYER, RESUME, SEARCH, AUTOSEARCH, PLATFORMS, SSR_SERVER})
@Test(description = "Проверяем функционал автопоиска резюме. Для этого производим поиск резюме, сохраняем запрос в автопоиск. " +
"Открываем страницу с автопоисками, проверяем наличие автопоиска с искомым именем. Изменяем домен и проверяем, что автопоиск отображается.")
public void addResumeToAutoSearchTest() {
Employer employer = fixtureUtils.createEmployer();
searchResumeAndPutIntoAutosearch(employer, resume);
EmployerAutosearchResumeScreen employerAutosearchResumeScreen = screenFactory.createScreen(EmployerMainScreen.class)
.quickOpenScreen(EmployerAutosearchResumeScreen.class);
assertTrue(employerAutosearchResumeScreen.isResumeSubscribed(resume), "Автопоиск с искомым именем отсутствует");
employerAutosearchResumeScreen.changeDomain(Domain.RABOTA_BY);
employerAutosearchResumeScreen = screenFactory.createScreen(EmployerMainScreen.class)
.quickLogin(employer)
.quickOpenScreen(EmployerAutosearchResumeScreen.class);
assertTrue(employerAutosearchResumeScreen.isResumeSubscribed(resume), "Автопоиск с искомым именем отсутствует на JTB");
employerAutosearchResumeScreen.changeDomain(Domain.HH);
}
При большом количестве тестов особую роль для нас имеет стабильность и скорость.Чтобы уменьшить риски падения тестов из-за изменения локаторов, мы договорились с фронтенд-разработчиками о введении data-атрибутов (data-qa), которые по возможности остаются неизменными при любых изменениях вёрстки. Еще мы попросили добавить флаги в JS окружение, которые будут сигнализировать нам о завершении различных процессов.
Все это, конечно, здорово. Но создавать каждую сущность через UI — очень затратная по времени операция, поэтому мы написали приложение hh-fixture — это rest-сервис, задача которого создавать различные сущности в базе данных. Вместе с Kraken они позволяют всем нашим тестам укладываться в 50 минут.
О стабильности и времени прохождения тестов лучше судить наглядно, поэтому результаты запуска мы сохраняем в базу, поверх которой крутится портал со статистикой. Вот, например, наши релизы за 2 недели:
Также держать руку на пульсе нам помогает бот, который периодически сообщает ответственным о времени прохождения тестов и составляет топ самых нестабильных.
Запускать все тесты каждый раз — излишне, поэтому мы используем наборы тестов. И далеко не всегда очевидно, как лучшим образом составить набор тестов под конкретный сервис, и какие тесты действительно его тестируют. Чтобы решить эту проблему, мы придумали систему с лейблами, можно думать о ней, как об облаке тегов. На каждый тест автором навешивается список тегов, по которым происходит выбор тестов для набора. Таким образом для большинства сервисов запускается ограниченное количество тестов, а время ожидания сокращается с 50 до 15 минут.
Ну вот и всё
На этом краткий экскурс в наши E2E-тесты завершён. Про интеграционные, нагрузочные и контрактные разновидности расскажу в других статьях. Stay, как говорится, tuned!