Кейс: автоматизация тестирования верстки сайта с помощью скриншотов
Привет, меня зовут Фахридин Джамолидинов, я специалист департамента тестирования в «Ростелеком ИТ». Занимаюсь автоматизацией тестирования основного сайта компании rt.ru и сегодня я расскажу, как про мы внедрили тестирование скриншотами, или как помочь автотестам «видеть» ошибки.
Наш сайт – это не только витрина для информирования клиентов и продаж услуг и товаров для сегментов B2C, B2B и B2O, он ещё предназначен для обслуживания текущих клиентов: FAQ, чат, формы обратной связи, платежные формы и т.п. Он постоянно обновляется, и каждый раз после выпуска новой версии нужно проверять сотни страниц с богатым, динамичным UI на работоспособность в браузерах и адаптивность вёрстки.
Конечно же на этапе разработки применяется практика design review со стороны наших продуктовых дизайнеров, но нет-нет да проблемы всплывали: где-то элементы перекрываются, съезжаются, не сохраняется единый стиль. Осознавая, что ручное регрессионное тестирование существующего UI превращается в кошмар и отнимает кучу времени, мы решили автоматизировать данный процесс.
Как всё начиналось
Рассматривали разные варианты, от готовых отдельных систем управления тестами до вариантов генерации разметки страниц на лету. Хотелось, чтобы это было что-то простое в использовании и быстрое в работе. Полностью готовые системы практически сразу отбросили, так страниц и форм становилось всё больше, большинство из них уже были реализованы в имеющихся функциональных автотестах. Проще говоря, мы не хотели дублировать один и тот же код со своей спецификой и синтаксисом и дублировать действия в сторонней системе.
Вариант генерации разметки на лету тоже не подошел (если вкратце – нам просто это показалось достаточно трудоёмким процессом), поэтому мы решили, что лучше будет сделать и поддерживать в будущем своё решение с нужным функционалом и форматом отчета (благо есть готовые библиотеки, которые можно легко встраивать и изменять под свои нужды, велосипед – это не всегда плохо).
Итого, что нам требовалось:
возможность сравнения всей страницы целиком, отдельных элементов, групп элементов;
проверка на адаптивность (десктоп – 1920px, планшет – 768px, мобила – 360px);
поддержка минимум двух браузеров: Chrome и Firefox;
игнорирование и скрытие элементов (групп элементов) при сравнениях;
удобочитаемый отчет с подсветкой отличий.
В качестве стека технологий было решено использовать то, на чём ранее были реализованы функциональные тесты:
Java – основной язык программирования;
TestNG – фреймворк автоматизации тестирования;
Maven – сборщик;
Selenide – обертка над Selenium, которая упрощает написание веб автотестов;
Selenoid GGR – сервер-балансировщик для запуска изолированных браузеров в Docker контейнерах;
Allure – фреймворк для создания отчётов автотестов и плагин ScreenDiff;
aShot – библиотека для попиксельного сравнения изображений.
Благодаря балансировщику Selenoid GGR (Go Grid Router), в котором настроено распределение на несколько машин, мы имеем возможность поддержки нескольких браузеров (при необходимости: нескольких версий), плюс запуск тестов (изолированных браузеров) в параллель (~40 потоков).
В этой статье мы покажем как работает автотестирование по скриншотам – будем акцентировать внимание на функционал aShot (кстати, спасибо ребятам из Яндекса за такие полезные инструменты как aShot и Allure) и на самом процессе.
Как это работает
Тесты условно разбиты на две группы:
fullscreen – страницы целиком (с прокручиванием);
element(s) – отдельный элемент (группа элементов)
Каждая из групп представляет из себя data-driven тесты, т.е. каждая страница, элемент (группа элементов) – это отдельный параметризованный тест на основе DataProvider, а вся задача сводится к сравнению двух скриншотов. Например, регресс верстки списка Landing Page, в виде схемы выглядит так:
Избавляемся от динамики
Динамика в основном проявляется в виде:
каретки в полях ввода, которая мигает (может пропасть или появиться);
CSS Transition и Animations;
видео с автопроигрыванием и ряд других моментов.
Там, где есть возможность – отключаем (инъекцией CSS):
Каретку делаем прозрачной (задаем значение цвета = transparent
). Таким образом она всегда будет невидимой и не будет влиять на результат. CSS Transition – обнуляем transitions-delay и transition-duration. CSS Animation – обнуляем animation-delay, animation-duration и ставим состояние анимации в паузу, устанавливая animation-play-state
в paused.
*, *::after, *::before {
caret-color: transparent !important;
transition-delay: 0s !important;
transition-duration: 0s !important;
animation-delay: 0s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
overflow: hidden; // Опционально скрываем полосу прокрутки
}
Там, где нет возможности отключить – игнорим, либо удаляем со страницы при помощи js перед снятием скриншотов, потом отдельно проверяем другими тестами.
Проверяем на адаптивность
Если с десктоп браузером всё понятно, с планшетом и мобилой встал вопрос, как проще эмулировать браузер. Было решено, что пока достаточно использовать фичу вебдрайвера – mobileEmulation. Пример скрипта поднятия вебдрайвера Chrome с включением этой опции, в режиме планшета и выбором девайса = iPad:
@Override
public WebDriver createDriver(DesiredCapabilities capabilities) {
WebDriverManager.chromedriver().setup();
Map<String, String> mobileEmulation = new HashMap<>();
mobileEmulation.put("deviceName", "iPad");
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setExperimentalOption("mobileEmulation", mobileEmulation);
WebDriver driver = new ChromeDriver(chromeOptions);
WebDriverRunner.setWebDriver(driver);
return driver;
}
Снимаем скриншоты
Эталон мы берем, прогоняя тесты на продовском окружении (предварительно проверенный человеком). Получаем оттуда картинки наших страниц, элементов/групп элементов, которые будем сравнивать, применяя предварительно фильтры избавления от динамики (останавливаем анимации, скрываем элементы), и кладем их в соответствующие папочки:
screens / {browser} / {desktop | tablet | mobile} / {region} в зависимости от выбранного для запуска браузера, устройства (режима адаптивности) и регионов (некоторые компоненты могут быть регионозависимы, т.е. отображаться по-разному в зависимости от выбранного пользователем региона).
Название файла состоит обычно из:
для fullscreen (страницы целиком) = {URL path} + опционально {query}, {ref};
для отдельного элемента | групп элементов = {URL path} + {уникальный class | id | path} (тут на усмотрение кому как удобно, главное, чтобы можно было выделить уникальное значение)
Если при запуске был включен флаг обновления эталона, при прогоне тест будет предварительно идти на продовское окружение и обновит соответствующий эталонный скриншот.
Текущий скрин снимаем в процессе прогона на выбранном контуре, применяя также предварительно фильтры избавления от динамики и дополнительно указываем игнорируемые элементы (об этом ниже более подробно с примерами).
Пример снятия скриншота всей страницы (fullscreen):
Создаем объект Screenshot AShot, перед вызовом метода takeScreenshot() вызываем метод shootingStrategy(), в параметрах которого задаем правило съемки viewportPasting(300), т.е. снимать скриншот с прокручиванием, с таймаутом 300 мс. AShot по умолчанию использует jQuery для поиска координат, при его отсутствии, необходимо включить поиск координат с помощью WebDriver API предварительно вызвав метод coordsProvider(new WebDriverCoordsProvider()).
Screenshot actualScreenshot = new AShot()
.shootingStrategy(ShootingStrategies.viewportPasting(200))
.coordsProvider(new WebDriverCoordsProvider())
.takeScreenshot(getWebDriver());
На этапе снятия скриншотов в режиме планшета и мобилы, пробовали разные модели устройств и столкнулись с проблемами, что при прокрутке то масштаб выходил за рамки, то появлялись черные области на скринах, посмотрели открытые Issues проекта AShot, оказалось у многих возникает такая ситуация:
Погуглив и проанализировав DPR (Device Pixel Ratio) значения страниц в разных режимах, стало понятно, что дело скорее всего в соотношении пикселей устройств, которые мы эмулируем. Запустив window.devicePixelRatio;
в консоли браузера, можно найти его. Задали другой shootingStrategy, указав подходящий устройству DPR scaling в параметрах, и таким образом избавились от вышеуказанных проблем.
Для режима планшет (iPad) указали ShootingStrategies.viewportRetina(600,0,0,2f).
Для мобилы (Pixel 2XL) указали – ShootingStrategies.viewportRetina(600,0,0,3.5f)
Пример снятия скриншота конкретного элемента (группы элементов):
При вызове метода takeScreenshot(), в параметрах, помимо текущего WebDriver, передаем еще элемент WebElement либо коллекцию элементов Collection <WebElement>, по которым мы хотим получить скриншот:
Collection<WebElement> elements = new ArrayList<>();
elements.add($x("//div[contains(@class,'rtk-offer-list')]")); // допуслуги и оборудование
elements.add($x("//div[@class='rtk-offer__legend']")); // блок цены
Screenshot actualScreenshot = new AShot()
.shootingStrategy(ShootingStrategies.viewportPasting(200))
.coordsProvider(new WebDriverCoordsProvider())
.takeScreenshot(getWebDriver(),elements);
Снятие скриншота c установкой игнорирований элемента(-ов) при сравнении:
Создаем set из элементов, которые хотим игнорировать при сравнениях:
Set<By> ignoredElements = new HashSet();
ignoredElements.add(By.xpath(xpath);
и передаем при снятии актуального скриншота этот набор вызовом метода ignoredElements(final Set ignoredElements)
Screenshot actualScreenshot = new AShot()
.shootingStrategy(ShootingStrategies.viewportPasting(200))
.coordsProvider(new WebDriverCoordsProvider())
.ignoredElements(ignoredElements)
.takeScreenshot(getWebDriver());
чтобы игнорирование применилось и для эталонного скриншота, до сравнений, указываем для него набор игнорируемых областей, согласно набору игнорируемых элементов текущего скриншота:
expectedScreenshot.setIgnoredAreas(actualScreenshot.getIgnoredAreas());
Сравнение скриншотов
После того как был получен актуальный скриншот, применены правила и фильтры к нему и эталонному скриншоту, сопоставляем их и аттачим в отчёт.
ImageDiff diff = new ImageDiffer().withDiffMarkupPolicy(new ImageMarkupPolicy().withDiffColor(Color.RED)).makeDiff(expectedScreenshot, actualScreenshot);
if(diff.getDiffSize()!=0){
Allure.label("testType", "screenshotDiff");
attachImg("expected", expectedScreenshot.getImage());
attachImg("actual", actualScreenshot.getImage());
attachImg("diff", diff.getMarkedImage());
attachImg("diff (прозрачность)", diff.getTransparentMarkedImage());
Assert.fail("Скрины отличаются");
}
Для удобства просмотра отличий используем плагин Allure ScreenDiff, который добавляет специальный блок в сценарий, внутри отчёта, с двумя режимами:
Отображение отличий с подсветкой (Show diff);
Отображение формы с режимом наложения (Overlay) с передвигаемым по горизонтали ползунком для удобного визуального сопоставления текущего и ожидаемого результата.
Благодаря использованию вышеописанного подхода мы теперь можем быть более уверенными в том, что сайт отображается корректно, адаптивность в порядке, а заодно пришли к автоматическому дизайн-ревью регресса.
А в чём польза?
Шрифт рендерится по-другому, слово стало на пару пикселей шире и текст перенесся на новую строку – возникает огромное количество расхождений? Подобные расхождения бывают безобидными, согласен, но благодаря недостаточному тестированию верстки, или высокому порогу погрешности можно получить результат как на скрине ниже.
Пользователи мобильной версии версии сайта бренда долгое время вместо кнопки «Узнайте больше» видели «Узнайте боль…» (спасибо ребятам из Альфа-банка, которые первыми это заметили). Это один из примеров, который вызывает больше улыбку чем неудобство, но бывают ситуации, которые не столь безобидны. На этом пока всё. Всем стабильных тестов! :)