Автоматические end-to-end тесты хороши тем, что позволяют сымитировать действия пользователя на сайте. Мы можем запрограммировать в скрипте теста действия типа открыть страницу, нажать на кнопку, ввести данные в поля ввода, нажать галочки и радиокнопки, отправить форму, и ждать на выходе нужный результат. Увидел текст "Ваше сообщение принято. Спасибо" - тест пройден. В ином случае - не пройден. Все прозрачно и понятно. Можно написать автотесты на все критично важные модели поведения пользователя на сайте, перед каждым обновлением кода на боевом сервере прогонять их и таким образом значительно повысить качество разработки. Но мы пойдем еще дальше.
Проблема
Тест проверяет логику, а значит, он не сможет выявить негативный эффект на сайте, вызванный изменениями в стилях: съехала шапка, скрылся важный блок, картинки улетели за пределы экрана, текст в соседних плашках наложился друг на друга. Особенно если регресс произошел на тех страницах, где его никто не ждет. Проводить визуальный осмотр всего сайта после каждой правки фронтенда - очень дорогое удовольствие. К тому же малоэффективное, ведь тестировщик должен держать в памяти все, что было на этой странице в прошлый раз. В большинстве случаев человек просто не заметит изменений, если они не были фатальными. А надо бы.
Идея
Нам нужен такой автотест, который будет проходиться по всем критично важным страницам сайта, делать скриншот каждой из них и сохранять куда-то в свое хранилище. При последующем тесте повторять эти действия и сравнивать два скриншота - прошлый и настоящий, лучше еще и с подсветкой изменений. Далее реализовать интерактивный веб-интерфейс, в котором отображались бы результаты тестов по всем нашим проектам, можно было зайти на страничку каждого из них, просмотреть скриншоты его страниц, сделанные с десктопного браузера, планшета и мобильного устройства. Если изменения, что произошли на страничке, являются фичей, а не багом, то нужна возможность отметить скриншот как некий эталон и при последующих прогонах теста сравнивать уже с ним. Если же произошел регресс страницы, то дать команду ответственному разработчику исправить баг. Затем снова прогнать тест, убедиться, что баг исправлен и при этом верстка не поехала где-то в другом месте. Возможность запускать автотест должна быть у каждого сотрудника компании.
Решение
Мы в своей практике давно и успешно пользуемся фреймворком Codeception для тестирования логики. Как оказалось, у него есть интересный плагин VisualCeption. Подробно изучив его возможности, мы поняли, насколько это крутая штука, и спешим поделиться с вами готовым решением на его основе.
Итак, у нас есть сайт - для примера возьмем вот эту бесплатную тему для админки с просторов интернета. Выберем штук 7 страниц, перед каждым деплоем на боевой сайт будем снимать с них скриншоты и смотреть на визуальный регресс. Сайт должен отображаться корректно на разрешениях экрана, соответствующих экрану десктопа (1920×1080px), планшета (768×1024px) и мобильного телефона (375×812px). Рабочим браузером будет Firefox.
Техническая часть
Практика показала, что запускать автотесты лучше на компьютере с видеопамятью. Для примера: тест на 100 страниц отрабатывает 50-60 минут на сервере и 12-15 минут на обычном офисном компьютере. Лучше выделить специальный компьютер под автотесты (далее - агент), сделать его доступным из веб-браузера, настроить доступ по SSH. Установить php с расширением imagick, а также docker, composer и docker-compose.
В корневой папке веб-сервера агента будет находиться React-приложение, которое будет тем самым веб-интерфейсом для управления результатами визуального тестирования. Склонируйте на агент вот этот репозиторий, запустите сборку. Содержимое папки dist
просто переместите в корневую папку веб-сервера:
Далее нам нужно где-то организовать непосредственно сами скрипты автотестов, по директории на каждый проект. Тут же создаем папку visual-autotesting
, клонируем туда этот репозиторий. Устанавливаем зависимости Composer. В файле tests/acceptance.suite.yml
видим адреса боевого сайта, препрода, тестового и дев-стенда:
В файле tests/acceptance/VisualRegressCest.php
в методе pageProvider()
прописаны те самые 7 страниц сайта:
Смотрим на метод tryToTest()
- это и есть непосредственно сам скрипт автотеста. Он открывает в браузере указанную страницу, ждет 3с, чтобы загрузились разные интерактивные элементы. Далее идет проверка на 404ю ошибку, и, если тест после этого не упал, идет непосредственно снятие скриншота.
В первый раз скриншот сразу идет в хранилище эталонов. Во второй и последующий разы скрипт видит, что скриншот эталона существует, делает новый скриншот и сравнивает его с эталоном. Результат сравнения в виде еще одной картинки .png также складывает в хранилище:
Запуск автотестов для удобства организован в docker-контейнерах для возможности параллельной работы. Команды прописаны в package.json:
Интеграция с Jenkins
Чтобы инструмент стал действительно народным, важно чтобы все сотрудники, даже далекие от разработки, могли легко запускать тесты. Для этого нужно организовать процесс в какой-либо системе непрерывной доставки типа Jenkins. Иначе сотрудники будут вынуждены работать в недружелюбной командной строке, а это мало кому понравится. Они всячески будут ее избегать, при каждом запуске будут отвлекать разработчиков, чтобы случайно не ввести что-нибудь не то. Тестирование люди хотят запускать нажатием кнопки.
Создаем новый item в Jenkins, тип - pipeline. Ставим галочку параметризированной сборки, добавляем параметры:
1. REPOSITORY_NAME_BACK, тип - Choice parameter, варианты - visual-autotesting. По мере добавления новых проектов будете добавлять их сюда как новые варианты
2, 3, 4. desktop, tablet, mobile, тип - Boolean parameter. Это указания, в каких разрешениях экрана снимать скриншоты для каждой страницы проекта.
Наконец, сам Groovy-скрипт пайплайна:
Много кода
//
// Job for running autotest by codeception
// requirements:
// yum install php php-imagick php-zip
// curl -sL https://rpm.nodesource.com/setup_12.x | bash -
// yum install -y nodejs
// mv /etc/php.d/20-curl.ini.disabled /etc/php.d/20-curl.ini
// wget -O /etc/yum.repos.d/docker-ce.repo https://download.docker.com/linux/centos/docker-ce.repomv docker-ce.repo
// yum install docker-ce docker-ce-cli containerd.io docker-compose
// systemctl enable --now docker
// usermod -aG docker Jenkins.App
pipeline {
agent {
label "codeception_test"
}
stages {
stage('Parameters') {
steps {
script {
properties([
parameters([
choice(
choices: ['visual-autotesting'],
name: 'REPOSITORY_NAME_BACK'
),
booleanParam(defaultValue: true, name: 'desktop'),
booleanParam(defaultValue: true, name: 'mobile'),
booleanParam(defaultValue: true, name: 'tablet')
])
])
PROJECT_PATH = "/var/www/html"
BRANCH_NAME = "master"
REPOSITORY_NAME_FRONT = "visual-autotesting.frontend"
}
}
}
stage('get_front') {
steps {
sh "sudo chown -R Jenkins.App:Jenkins.App ${PROJECT_PATH}"
echo "get sources for back"
checkout([$class: 'GitSCM',
branches: [[name: "${BRANCH_NAME}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'RelativeTargetDirectory',
relativeTargetDir: "${PROJECT_PATH}/${REPOSITORY_NAME_FRONT}"]],
submoduleCfg: [],
userRemoteConfigs: [[url: "https://github.com/MaDRaGe/${REPOSITORY_NAME_FRONT}"]]])
sh "sudo chmod g+w -R ${PROJECT_PATH}/${REPOSITORY_NAME_FRONT}/."
sh "sudo chown -R Jenkins.App ${PROJECT_PATH}/${REPOSITORY_NAME_FRONT}/."
}
}
stage('get_back') {
steps {
echo "get sources for back"
checkout([$class: 'GitSCM',
branches: [[name: "${BRANCH_NAME}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'RelativeTargetDirectory',
relativeTargetDir: "${PROJECT_PATH}/${REPOSITORY_NAME_BACK}"]],
submoduleCfg: [],
userRemoteConfigs: [[url: "https://github.com/Pum-purum/${REPOSITORY_NAME_BACK}.git"]]])
sh "sudo chmod g+w -R ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}/."
sh "sudo chown -R Jenkins.App ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}/."
sh "rm -rf ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}@tmp"
}
}
stage('build_front') {
steps {
echo "build front project"
sh '''
cd ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''
npm install --prefix ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''
npm run build --prefix ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''
mv ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''/dist/* ''' +PROJECT_PATH+ '''/
rm -rf ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''
rm -rf ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_FRONT+ '''@tmp''
ls -la .
'''
}
}
stage('build_back') {
steps {
echo "build back project"
sh "composer update --working-dir=${PROJECT_PATH}/${REPOSITORY_NAME_BACK}"
}
}
stage('testing') {
steps {
parallel (
"desktop" : {
script {
if (params.desktop) {
echo "desktop"
try {
sh '''
cd ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
sudo npm run desktop --prefix ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
ls -la .
'''
} catch (err) {
echo err.getMessage()
}
}
}
},
"tablet" : {
script {
if (params.tablet) {
sleep(time:10,unit:"SECONDS")
echo "tablet"
try {
sh '''
cd ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
sudo npm run tablet --prefix ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
ls -la .
'''
} catch (err) {
echo err.getMessage()
}
}
}
},
"mobile" : {
script {
if (params.mobile) {
sleep(time:20,unit:"SECONDS")
echo "mobile"
try {
sh '''
cd ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
sudo npm run mobile --prefix ''' +PROJECT_PATH+ '''/''' +REPOSITORY_NAME_BACK+ '''
ls -la .
'''
} catch (err) {
echo err.getMessage()
}
}
}
}
)
}
}
}
post {
always {
sh "sudo chmod g+w -R ${PROJECT_PATH}/."
sh "sudo chown -R 600:600 ${PROJECT_PATH}/."
sh "sudo chmod o+rw -R ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}/json/."
sh "sudo chmod o+rw -R ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}/tests/_data/."
sh "sudo chmod o+rw -R ${PROJECT_PATH}/${REPOSITORY_NAME_BACK}/tests/_output/."
}
}
}
Суть скрипта в том, что пользователь Jenkins.App
подключается к агенту codeception_test
, заходит в папку /var/www/html
, и выполняет все те действия, что мы руками делали в разделе Техническая часть. Чтобы пайплайн корректно запустился в вашем Jenkins, нужно наши значения заменить на ваши. Скорее всего, тут понадобится помощь системного администратора.
Результат
Заходим в браузере по адресу нашего веб-интерфейса и видим одинокий (пока) автотест visual-autotesting:
Нажимаем на его иконку и видим такую красоту:
У каждой страницы есть значок в левом меню, показывающий результат теста:
Зеленая галочка означает, что текущий скриншот полностью идентичен эталону;
Желтый восклицательный знак - что есть изменения;
Оранжевый крестик - что страница выдает 404ю ошибку, причем эта же ошибка была и в прошлый раз;
Красная молния означает, что страница выдает 404ю ошибку, но этой ошибки не было в прошлый раз;
При просмотре страниц меняется адрес в адресной строке браузера, таким образом, вы легко можете поделиться с коллегой ссылкой на проблемную страницу:
В случае, если автотест заметил изменения, действуем так. При клике на любую картинку всплывает попап с увеличенным изображением эталона, последнего скриншота и результата их наложения:
Если последний скриншот нам нравится больше, чем эталон, жмем на кнопку Отметить эталон
под этим скриншотом. После выбора эталона кнопка и картинка переезжают налево, давая понять, что здесь был отмечен эталон. Данное состояние сохраняется после перезагрузки страницы.
Проходим по всем страницам, отмечаем эталоны, по итогу тестирования ставим задачи разработчикам на исправление багов.
Всем продуктивной разработки и качественного тестирования!
Rive
Напомнило, как я делал самопальные тесты самопального распознавания текста с экрана: брал скриншот с особо неразборчивым текстом как эталон, запоминал его результат распознания, добавлял ещё один глиф в визуальный словарь и затем прогонял тесты заново.
Проект был больше для удовлетворения собственного любопытства, поэтому я почти не был обременён грузом объяснения этого велосипеда другим пользователям.