Как и зачем тестировать верстку
Допустим, вы разрабатываете проект с “богатым наследием”, содержащий огромное количество страниц и старого кода. Вы сдвигаете на пару пикселей блочок… и бах! На другой странице что-то разваливается. Знакомая ситуация? Или, быть может, вы пишете новый проект, постоянно меняете и добавляете что-то в свою библиотеку компонентов, и у вас нет времени прокликивать все имеющиеся страницы, чтобы убедиться, что новая функциональность нигде ничего не отломала. В обоих случаях вам пригодится тестирование верстки на визуальные регрессии.
Регрессионное тестирование — это вид тестирования, направленный на проверку того факта, что уже имеющаяся функциональность работает как раньше.
В качестве инструмента для регрессионного тестирования верстки я выбрала BackstopJS.
Что он умеет
- Тестировать отдельные блоки
- Скрывать блоки со страницы
- Эмулировать действия пользователя
- Показывать отчеты о результате тестов в браузере и в CLI
- Интегрироваться с CI
и многое другое.
Имеются аналоги, например, Selenium, PhantomCSS, Gemini и другие. Но для решения моих задач отлично подошел именно BackstopJS, и на его настройку у меня ушло совсем немного времени.
Из чего он состоит
- Chrome Headless, Phantom или Slimer — для рендеринга
- Puppeteer, ChromyJS или CasperJS — для эмуляции действий пользователя
- Resemble.js — для анализа, сравнения скриншотов
Начало работы
Устанавливаем:
$ npm install -g backstopjs
Инициализируем дефолтную конфигурацию:
$ backstop init
Для тестирования достаточно двух команд:
$ backstop test
$ backstop approve
Процесс тестирования
Процесс тестирования во многом напоминает знакомую с детства игру “найди 10 отличий”. У вас есть эталонные скриншоты и текущие скриншоты. Они накладываются один на другой и фиксируется разница. Если они отличаются — тест не проходит. Тогда, в зависимости от того, является ли изменении ожидаемым или нет, разработчик либо фиксит баг, либо принимает изменения и обновляет эталонные скриншоты.
Пример теста
Тестовые сценарии описываются в backstop.json, который создается в корне проекта. Давайте посмотрим, что в нем есть.
id — для именования скриншотов
viewports — экраны, для которых будут сниматься скриншоты
Например,
"viewports": [{
"label": "phone",
"width": 320,
"height": 480
},
{
"label": "tablet",
"width": 1024,
"height": 768
},
{
"label": "notebook",
"width": 1440,
"height": 900
},
{
"label": "large screen",
"width": 1920,
"height": 1200
}]
onBeforeScript — служит для того, чтобы задать состояние браузеру перед началом тестирования
onReadyScript — скрипт, который задает некоторое состояние для скриншотов, например hover на выбранных элементах
Эти скрипты находятся в папке backstop_data/engine_scripts.
asyncCaptureLimit, asyncCompareLimit — позволяет параллельно снимать несколько скриншотов и параллельно сравнивать их соответсвенно
По умолчанию значения 10 и 50 соотвественно, это ускоряет прохождение тестов, но иногда приводит к потере качества. Я выбрала (эмпиричеки) значения 2 и 5.
scenarios — массив тестовых сценариев
Пример одного такого сценария:
{
"label": "Home page",
"url": "http://localhost:3000/home",
"readySelector": ".backstopReadySelector",
"delay": 0,
"misMatchThreshold" : 0.1,
"requireSameDimensions": true
}
Здесь обязательными параметрами будут label — будет в названии скриншота, по нему будут сравниваться тестовые скриншоты с эталонными и url — по которому находится страница, которую будем тестировать.
Все остальное является опциональным. Полный список можно посмотреть здесь.
Запускаем команду $ backstop test
Сгенерятся тестовые скриншоты и положатся в папку bitmaps_test/<timestamp>/
С эталонными скриншотами, лежащими в папке bitmaps_reference
, будут сравниваться самые свежие тестовые скриншоты.
По окончании тестов, в CLI вы увидите такой отчет
И в браузере откроется результат тестирования (указано в настройках по умолчанию):
На визуальном отчете ярко-розовым подстветится разница между эталонными и тестовыми скриншотами. Если есть разница — тест не прошел. Если это те изменения, которые вы и ожидали (добавилась новая функциональность) — надо обновить эталонные скриншоты.
Запускам $ backstop approve
Теперь последние тестовые скриншоты сохранены как эталонные и в следующий раз сравнение будет уже с ними. Эталонные скриншоты есть смысл хранить в VCS, так они привязаны к ветке, к ним есть доступ у всех членов команды и можно закинуть их в ревью — так сразу видно, что визуально изменилось.
Эмулируем действия пользователя
Допустим, вам надо протестировать кнопку в различных состояниях или поднять попап. BackstopJS дает такую возможность.
Для эмуляции клика добавляем в сценарий следующее:
clickSelector: ".my-hamburger-menu"
Для hover-a:
hoverSelector: ".my-hamburger-menu .some-menu-item"
Их также можно использовать вместе, вы фактически говорите “подожди, пока создастся элемент .my-hamburger-menu
, нажми на него, подожди, пока создастся элемент .my-hamburger-menu .some-menu-item
и наведи на него мышь”.
Правда если вам нужны более сложные сценарии, например, последовательные клики, придется написать свой скрипт.
Вот пример моего скрипта, который делает последовательно несколько кликов (кот в консоли для красоты):
//sequentialClicksHelper.jsmodule.exports = function (engine, scenario, vp) {
console.log('Running custom scenario...\n' +
' /\\__/\\\n' +
' /` \'\\\n' +
' === 0 0 ===\n' +
' \\ -- /\n' +
' / \\\n' +
' / \\\n' +
' | |\n' +
' \\ || || /\n' +
' \\_oo__oo_/#######o' +
'');
var clickSelector1 = scenario.clickSelector1;
var clickSelector2 = scenario.clickSelector2;
if (clickSelector1 && clickSelector2) {
engine
.wait(clickSelector1)
.click(clickSelector1)
.wait(clickSelector2)
.click(clickSelector2)
.wait(250);
}
};
Я положила его в папку backstop_data/engine_scripts
(кстати, если вы используете не ChromyJS (а, например, CasperJS — это тулзы, эмулирующие действия пользователя, входят в состав BackstopJS), то синтаксис будет немного отличаться. Посмотрите примеры скриптов в папке).
В сам сценарий скрипт подключаем так:
"onReadyScript": "sequentialClicksHelper.js",
"clickSelector1": ".qaPurchaseActions button",
"clickSelector2": ".qaPurchaseActions a"
Скриншотим страницы с динамически изменяющимся контентом
Если контент на вашей странице меняется, например, вы отправляете аякс запрос и, пока не получите ответ, показываете спиннер, вам надо как-то отскриншотить именно страницу в определенном состоянии. BackstopJS предлагает несколько путей:
- скрыть элементы
- задержка
- ready event
- ready selector
Можно выбрать селекторы и скрыть их со страницы:
"hideSelectors": [
"#someFixedSizeDomSelector"
]
Можно выставить задержку в милисекундах перед снятием скриншота:
"delay": 1000 //delay in ms
Свойство readyEvent
позволяет задать строку, которая будет выведена в console.log(). Пока строка не выведена в консоль, скриншот снят не будет:
"readyEvent": "backstopjs_ready"
Свойство readySelector
говорит BackstopJS подождать, пока на странице не появится указанный селектор, и только потом снимать скриншот.
{
"label": "Home page",
"url": "http://localhost:3000/home",
"readySelector": ".backstopReadySelector"
}
Тестирование в разных системах
Когда мы все настроили и начали тестировать, то первая проблема, с которой столкнулись: один член команды снимать скриншоты, сохраняет их как эталонные, заливает, другой скачивает их, запускает тесты и бах! Ни один тест не проходит. Потому что один использует MacOS, а другой — Ubuntu. На разных системах сайт будет рисоваться по-разному, как ни крути. Например, будут отличаться шрифты.
Выход: тестировать в докер-контейнере. Вы просто берете готовый образ и прогоняете все тесты в одной и той же среде.
Устанавливаете docker, затем выполняете:
$ docker run --rm -v $(pwd):/src backstopjs/backstopjs --version
BackstopJS v3.x.x
Тесты могут выглядеть так:
docker run --rm -v $(pwd):/src backstopjs/backstopjs init
docker run --rm -v $(pwd):/src backstopjs/backstopjs reference
docker run --rm -v $(pwd):/src backstopjs/backstopjs test
Подводя итог, зачем тестировать верстку на визуальные регрессии:
- Удобство для членов команды
- Делает процесс разработки более предсказуемым
- Ускоряет процесс разработки, тестирования и релиза, потому что помогает выявить свои же косяки на ранней стадии, а не ждать тестирования QA.
Я лично считаю, что такой вид тестов — для удобства самого разработчика. Они помогают автоматизировать нудный процесс проверки, что нигде ничего не отломалось, а ведь мы все хотим делать качественный продукт. Такие тесты носят рекомендательный характер, когда они падают — это нормально. Разрабочик сам принимает решение — что ок, а что нет. Процесс тестирования можно организовать по-разному, главное — чтобы вам самим было удобно. Потому что в конечном итоге это тесты для вас, и если вам неудобно, то и смысла в них особого нет.