Как и зачем тестировать верстку

Допустим, вы разрабатываете проект с “богатым наследием”, содержащий огромное количество страниц и старого кода. Вы сдвигаете на пару пикселей блочок… и бах! На другой странице что-то разваливается. Знакомая ситуация? Или, быть может, вы пишете новый проект, постоянно меняете и добавляете что-то в свою библиотеку компонентов, и у вас нет времени прокликивать все имеющиеся страницы, чтобы убедиться, что новая функциональность нигде ничего не отломала. В обоих случаях вам пригодится тестирование верстки на визуальные регрессии.

Регрессионное тестирование — это вид тестирования, направленный на проверку того факта, что уже имеющаяся функциональность работает как раньше.

В качестве инструмента для регрессионного тестирования верстки я выбрала 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 отличий”. У вас есть эталонные скриншоты и текущие скриншоты. Они накладываются один на другой и фиксируется разница. Если они отличаются — тест не проходит. Тогда, в зависимости от того, является ли изменении ожидаемым или нет, разработчик либо фиксит баг, либо принимает изменения и обновляет эталонные скриншоты.

Игра “Найди 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 вы увидите такой отчет

Зафейлилось 17 тестов, прошло 21

И в браузере откроется результат тестирования (указано в настройках по умолчанию):

Отчет в браузере

На визуальном отчете ярко-розовым подстветится разница между эталонными и тестовыми скриншотами. Если есть разница — тест не прошел. Если это те изменения, которые вы и ожидали (добавилась новая функциональность) — надо обновить эталонные скриншоты.

Пример визуального отчета для зафейленного теста

Запускам $ 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.js
module.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.

Я лично считаю, что такой вид тестов — для удобства самого разработчика. Они помогают автоматизировать нудный процесс проверки, что нигде ничего не отломалось, а ведь мы все хотим делать качественный продукт. Такие тесты носят рекомендательный характер, когда они падают — это нормально. Разрабочик сам принимает решение — что ок, а что нет. Процесс тестирования можно организовать по-разному, главное — чтобы вам самим было удобно. Потому что в конечном итоге это тесты для вас, и если вам неудобно, то и смысла в них особого нет.

Полезные ссылки