Как заставить тесты «видеть» ошибки: элегантный способ автоматизации тестирования

Меня зовут Алексей Лакович, я занимаюсь автоматизацией тестирования в компании Генезис. В тестировании я работаю уже более 6 лет и накопил очень большой опыт как в автоматизации тестирования, так и в организации команд по ручному тестированию.

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

Хочу вам рассказать про одно из элегантных решений на тему визуального тестирования сайта/приложений. Я совсем недавно рассказывал про него на наших Беседах (мы раз в неделю или приглашаем в компанию внешнего спикера, или кто-то из компании делает выступление на какую-то из тем). Мне показалось, что про такое решение будет интересно узнать тем читателям DOU, которые занимаются автоматизацией тестирования.

Немного воды. Ручное тестирование я невзлюбил еще на первой неделе своей работы. Так как лень меня начала побеждать, и на этапе задачи «мы там выкатили пару фиксов/новый функционал/поменяли текст/{любое другое}, посмотри, пожалуйста, все ли ок?», я задумался — а ведь можно нехилый кусок такой рутинной работы автоматизировать. Кроме этого, после выкатки обновлений периодически какая-нибудь ссылка, либо картинка в футере на странице «Контакты» едет, и это может остаться незамеченным многие дни, а то и недели.

Данным способом «визуального тестирования» можно обнаруживать ошибки верстки, текстов, различия PROD и STAGE версий, отличия разных версий браузеров, а также все недочеты, которые можно «увидеть» при тестировании ПО.

Собственно идея проста: сравнение скриншотов приложения (Actual vs Expected) и получение разницы между ними.

У себя мы используем язык Java, Selenium WebDriver и очень неплохое решение от ребят из Яндекса — библиотеку aShot (огромный им привет и благодарность за такие инструменты, как aShot и Allure).

Если вы разрабатываете ваши тесты на других языках — подход остается тем же, так как основной принцип не меняется, а варианты технического решения всегда найдутся:
— Написать аналогичный инструмент для сравнения картинок.
— Воспользоваться уже имеющимися решениями (погуглить либо посмотреть в сторону универсального инструмента для работы с изображениями, например, imagemagick).

Логика теста. Первое, о чем хотелось бы написать — это о незамысловатой «логике» работы самого теста:
1. Браузер переходит по страницам и делает скриншоты страниц/элементов.
2. Если отсутствует эталонный снимок, то сделанный скриншот сохраняется в папку с «expected» скриншотами.
3. Далее вызывается метод для сравнения картинок (только что сделанного и эталонного снимков) и выдает результат — различие между ними в пикселях;
4. Если это значение == 0, то тест проходит успешно. Если же нет, то собирается гифка из скриншота эталона, актуального снимка и снимка с помеченными отличиями.
5. Все это дело формируется в отчет со всей информацией о тесте и сделанными снимками, и в случае провала приходит уведомление на почту.

Помимо этих шагов, конечно, есть дополнительные условия, такие как заданные допустимые отличия (числовое значение пикселей), игнорирование динамических элементов, подмена значений каунтеров/текста на странице — любых вещей, которые «мешают» или не особо важны.

Но в самом базовом случае можно обойтись и без лишних заморочек.

Также при первом запуске теста вы должны быть уверены, что ваш сайт отображается «правильно», и те снимки, которые были сделаны в процессе, будут действительно эталонными.

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

Для начала определимся с основными частями реализации, которая нам нужна для написания подобных тестов:
1. Установить, настроить систему для работы.
2. Продумать структуру папок для хранения наших скриншотов.
3. Подумать над формированием имени файлов (не стоит недооценивать этот момент).
4. Делать снимок приложения в процессе теста.
5. Собственно сравнивать скриншоты Actual и Expected:
— иметь набор правил для валидации результата сравнения (допустимые отличия, игнорирование некоторых элементов);
— выводить результат сравнения в удобном для просмотра виде, чтобы не пришлось глазами искать разницу между снимками.

Установка/настройка окружения

Для того чтобы у нас все было готово к работе, мы должны установить:
1. JDK;
2. IntelliJ IDEA (+ драйвер для Хрома);
3. Maven (конечно, можно без него; по дефолту в IDEA он должен быть);
4. Selenium WebDriver;
5. Драйверы для нужных нам браузеров (если крутить будем локально);
6. TestNG/JUnit (тут тоже опционально);
7. Yandex AShot.

Это упрощенная версия, все подробности по настройке каждого из пунктов можно найти в интернете.

Не хочется много писать про установку каждого пункта, но опишу кое-что из важного. Скачиваем JDK, устанавливаем «IntelliJ IDEA», запускаем и создаем новый «Maven Project». Затем переходим к подключению нужных нам библиотек и фреймворков, подключаем зависимости в мавен pom.xml файл:

Selenium WebDriver

<!-- Selenium java client -->
<dependency>
   <groupId>org.seleniumhq.selenium</groupId>
   <artifactId>selenium-java</artifactId>
   <version>3.0.1</version>
</dependency>
<!-- Selenium Server -->
<dependency>
   <groupId>org.seleniumhq.selenium</groupId>
   <artifactId>selenium-server</artifactId>
   <version>3.0.1</version>
</dependency>

TestNG

<!--TestNG -->
<dependency>
   <groupId>org.testng</groupId>
   <artifactId>testng</artifactId>
   <version>6.9.10</version>
</dependency>

AShot

<!-- Yandex AShot -->
<dependency>
   <groupId>ru.yandex.qatools.ashot</groupId>
   <artifactId>ashot</artifactId>
   <version>1.5.2</version>
</dependency>

Структура папок

Мы должны продумать структуру папок для хранения снимков. Самый простой вариант — взять какую-либо корневую директорию с названием типа «testScreenshots» и в ней создать 4 директории под скриншоты. Данный пункт лучше реализовать программно, чтобы не приходилось в случае запуска теста на другой тачке создавать их вручную:

{yourMainDir}/testScreenshots/expected/ — папка с эталонными скриншотами;
{yourMainDir}/testScreenshots/actual/ — папка со скриншотами, сделанными в процессе выполнения теста;
{yourMainDir}/testScreenshots/markedImages/ — папка с наложенными друг на друга снимками и помеченными различиями между ними;
{yourMainDir}/testScreenshots/gifs/ — папка для хранения gif изображений (для удобства просмотра отличий будем склеивать expected, actual и markedImages).

Также можно для каждых отдельных наборов тестов добавить еще одну папку в адрес, и будет что-то типа — {yourMainDir}/myAwsomeTestSuite/testScreenshots/

В коде по этому пункту проблем быть не должно — записываем пути в переменные (expected, actual, markedImages, gifs) и делаем метод setter, который их назначает в случае, если для конкретного теста мы хотим изменить директорию, и соответственно создает эти папки в случае их отсутствия в системе.

Пример setter’а:

public void setRootScreenshotsDir(String absolutePath){
   resourcesImagesDir= absolutePath;
   expectedDir = resourcesImagesDir+"/expected/";
   actualDir = resourcesImagesDir+"/actual/";
   diffDir = resourcesImagesDir+"/diff/";
   resultGifsDir = resourcesImagesDir+"/gifs/";
   createFolders();// Метод который проверяет наличие папок file.exists() , в случае их отсутствия - создает (file.mkdirs())
}

Названия тестовых скриншотов

Название файла скриншота — это его уникальный идентификатор, по которому мы будем искать нужный нам эталонный скриншот. Это довольно важный момент, так как тесты могут быть написаны под разные браузеры и размер окон. Мы должны это учесть в назначении имени наших сохраняемых скриншотов.

У нас это делается следующим образом — наименование скриншота состоит из:
1. имя домена (слитно) — hitwecom;
2. название страницы\элемента — profile;
3. локализация — EN;
4. браузер — Chrome;
5. размер окна браузера — 1366×768;
6. любой другой параметр, который вам может понадобиться для идентификации снимка, например, версия браузера.

Ну и собственно сам файл будет иметь название hitwecom_profile_EN_Chrome_1366x768.png, либо для другого браузера/размера окна — hitwecom_profile_EN_Firefox_1280x1024.png.

В самом тесте при вызове метода для сравнения скриншотов приходится указывать только название элемента. Все остальные данные берутся автоматически перед сохранением файла с помощью driver().getCurrentUrl(); driver().manage().window().getSize(); параметрами, которые передаем в тест, такие как браузер и локализация.

Также тут стоит учесть, чтобы название файлов не отличалось при прогоне ваших тестов на стейдж сервере и на проде. Будет довольно удобно иметь заготовленные скриншоты с прода и сравнивать с тем, что пытаются выдать как «новый релиз» на стейдже.

Снятие и сохранение скриншота

Скриншот можно получать любым удобным для вас способом, данный «вид» тестирования можно применять не только для веб-приложений. Единственное отличие, которое у вас будет, — это то, как вы получаете сам снимок и проставляете координаты для игнорируемых блоков. Остальные пункты могут совершенно не отличаться.

В библиотеке aShot имеется своя реализация для снятия скриншота страницы либо веб-элемента с помощью ВебДрайвера.

Также нужно учесть, что наведение мышью на ссылки/блоки может влиять на внешний вид приложения, поэтому простым способом перед началом наших тестов просто уводим курсор в левый верхний угол экрана:

Robot bot = new Robot();
bot.mouseMove(0, 0);

Для страницы это выглядит следующим образом :

Screenshot screenshot1 = new AShot().takeScreenshot(driver);
// получаем скриншот страницы

Так как, к примеру, Хром не делает скриншот по всей высоте страницы — мы можем добавить к снятию скриншота shootingStrategy с аргументом scrollTimeout (значение в миллисекундах). Это задержка между скроллом и снятием следующей части страницы.

Снимок страницы со скроллом:

Screenshot screenshot = new AShot().shootingStrategy(ShootingStrategies.viewportPasting(100)).takeScreenshot(driver);

Если у вас «фиксированный хедер» (как у нас на скрине выше), и при скролле он всегда находится вверху страницы, то его можно закрепить перед снимком, изменив ему атрибут «style»:

Если нужно сделать скриншот элемента, а не всей страницы — мы можем передать в метод takeScreenshot конкретный элемент и сделать его снимок:

Скриншот веб-элемента:

WebElement header = driver.findElement(By.cssSelector(".header"));
Screenshot screenshot = new AShot().takeScreenshot(driver, header);

Важно: по умолчанию для определения координат элемента используется jQuery. Если у вас на проекте отсутствует поддержка jQuery, то нужно указать использование WebDriverCoordsProvider для их определения с помощью WebDriver API:

Screenshot screenshot = new AShot().coordsProvider(new WebDriverCoordsProvider()).takeScreenshot(driver, header);

Сохраняем полученный скриншот в файл:

File actualFile = new File(actualDir+name+".png");
ImageIO.write(screenshot.getImage(), "png", actualFile);

Если же вы делаете скриншот с помощью других утилит, то получить объект Screenshot с нужным вам файлом можно, передав в конструктор класса Screenshot BufferedImage:

File file = new File("C:/myDir/page1.png");
BufferedImage image = ImageIO.read(file);
Screenshot screenshot = new Screenshot(image);

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

Тут у нас реклама и фото юзера на странице, которые могут изменяться:

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

К примеру, для того чтобы убрать рекламу, нам достаточно иметь метод типа:

public void removeElements(By by){
   List<WebElement> elementsList = driver.findElements(by);
   for(WebElement element : elementsList ) {            ((JavascriptExecutor)driver)
.executeScript("arguments[0].remove();", element);
   }
}

И передать в него селектор, например, рекламных блоков.

Если нам нужно изменить значение счетчика сообщений, мы можем вызвать метод для этих целей:

public void setElementAttribute(By by, String attr, String value){
   List<WebElement> elementList = driver.findElements(by);
   for(WebElement element : elementList ) {
       ((JavascriptExecutor)driver)
.executeScript("arguments[0].setAttribute('" + attr + "', '" + value + "');", element);
   }
}

Удобно находить уникальный селектор сразу для всех блоков рекламы либо каунтеров и редактировать их пачкой, для этого используем findElements метод. Так можно значительно уменьшить количество кода.

Ну и соответственно, если нам нужно изменить текст, то и для этого тоже у нас есть метод:

public void setElementText(By by, String text){
   List<WebElement> elementList = driver().findElements(by);
   for(WebElement element : elementList ){
       ((JavascriptExecutor)driver)
.executeScript("arguments[0].innerHTML = \""+text+"\";", element);
       WebDriverWait wait = new WebDriverWait(driver(), 5);
     wait.until(ExpectedConditions.textToBePresentInElement(element, text));
   }
}

Второй способ — игнорирование. Мы можем в метод для снятия скриншота передать Set<By> ignoredElements. Создаем Set из элементов, которые мы не хотим учитывать при сравнении скриншотов, и вызываем метод для снятия скриншота, заведомо передав этот список в AShot.

Перед снятием снимка будут найдены координаты и размер наших элементов на странице, и в момент сравнения эти элементы (точнее, координаты и размер блоков) не будут учитываться в результате.

Вот пример снятия скриншота с игнором некоторых элементов:

Set<By> setIgnoredElements = setIgnoredElements(By.cssSelector(".banner"), By.cssSelector(".userPhoto"));
Screenshot screenshot = new AShot().ignoredElements(setIgnoredElements).shootingStrategy(ShootingStrategies.viewportPasting(100)).takeScreenshot(driver());

Перед сравнением скриншотов у нас будут актуальный (actualScreenshot) и ожидаемый (expectedScreenshot) снимки. Все игнорируемые зоны сохранены в нашем actualScreenshot, и при сравнении нам нужно будет эти зоны передать в наш expectedScreenshot.

Вот скриншот после проставления игнорирования рекламных баннеров и блока с фото юзера по центру:

Осталась проблема со счетчиком сообщений — он помечается как «ошибка». Его мы просто уберем. Вызываем наш метод для удаления элементов перед снятием скриншота и делаем снимок:

removeElements(By.cssSelector(".counter"));
Set<By> setIgnoredElements = setIgnoredElements(By.cssSelector(".banner"), By.cssSelector(".userPhoto"));
Screenshot screenshot = new AShot().ignoredElements(setIgnoredElements).shootingStrategy(ShootingStrategies.viewportPasting(100)).takeScreenshot(driver());

Теперь у нас есть чистая страница, готовая к прогону тестов без лишних «шумов»:

Сравнение скриншотов

Для начала получаем актуальный снимок, как из примеров выше:

Screenshot actualScreenshot = new AShot().takeScreenshot(driver); // сохраним его для отчета
ImageIO.write(actualScreenshot.getImage(), "png", new File(actualDir+name+".png"));

После снятия актуального скриншота мы проверяем, есть ли у нас эталонный снимок для этой страницы. Если его нет, то записываем актуальный как эталонный.

Еще было бы неплохо сделать возможность автоматически «перезаписать» все эталонные снимки. Это бывает нужно, когда, например, у нас кардинально изменили дизайн либо добавили новый блок. Для этого создаем какой-нибудь boolean параметр, типа newScreenshots, и если при запуске теста мы передадим true, то перезаписываем снятый скриншот в папку expected.

После чего мы должны поднять файл с нашим эталоном для сравнения:

Screenshot expectedScreenshot = new Screenshot(ImageIO.read(new File(expectedDir+name+".png")));

Если мы установили игнорируемые зоны, то должны передать их в наш ожидаемый снимок:

expectedScreenshot.setIgnoredAreas(actualScreenshot.getIgnoredAreas());

Далее, с помощью класса ImageDiffer вызовем метод для сравнения этих скриншотов:

ImageDiff diff = new ImageDiffer().makeDiff(expectedScreenshot, actualScreenshot);

И, собственно, получим результат сравнения. Различие снимков в пикселях:

//return int diffPoint
diff.getDiffSize();

Обернем результат в Assert и получим готовый тест на проверку внешнего вида нашего приложения:

Assert.assertEquals(diff.getDiffSize(), 0);

После этого сгенерируем картинку с пометками отличий наших Actual и Expected и сохраним его в папку с markedImages:

File diffFile = new File(markedImages+name+".png");
ImageIO.write(diff.getMarkedImage(), "png", diffFile);

Добавим все скриншоты в отчет (в нашем случае это Allure):

//Attach images to report
AllureAttachments.attachScreen(expectedFile.getAbsolutePath(), "Expected: "+name);
AllureAttachments.attachScreen(actualFile.getAbsolutePath(), "Actual: "+name);
AllureAttachments.attachScreen(diffFile.getAbsolutePath(), "Differ: "+name);

Сгенерируем gif изображение из картинок expectedFile, actualFile, diffFile:

File[] filesArray = {expectedFile, actualFile, diffFile};
gifFile = GifSequenceWriter.createGIF(filesArray, resultGifs+name);

GifSequenceWriter — класс, в котором есть метод для генерации gif изображения из массивов файлов. Гифку тоже стоит прикрепить к нашему отчету.

В результате получается удобное изображение на котором мы можем сразу увидеть все отличия:

Проверка и вывод результатов

Про игнорирование и подмену элементов на странице я уже написал. Также можно придумать набор правил для результатов тестов. Если нет желания сильно заморачиваться с игнорированием или вырезанием элементов, то можно прописать правила типа:
— допустимые различия в пикселях (например, 16 или 2078; мигающий курсор на поле для ввода текста);
— допустимый диапазон в пикселях;
— процентное соотношение несовпадения между скриншотами по формуле: diffPoints / кол-во пикселей в снимке * 100;
— все, что меньше N пикселей, не считать ошибкой.

Ну, и собственно, вывод результата тестов — это одна из главных задач автоматизатора, а особенно если это тесты, которые проверяют верстку. Мы у себя используем Jenkins, в котором генерим Allure отчет со всеми параметрами теста, скриншотами, сделанными в процессе выполнения, gif изображением с отличием, логами браузера, куками и т.д.

Описывать, как настраивать эти дела, думаю, в этой статье не стоит, так как эта тема заслуживает отдельного материала.

Самое главное: выводите значение diffPoints и скриншоты — такой отчет уже будет очень полезен :)

Ну и засыпайте всех алертами на почту — это стимулирует фиксить тесты и баги :)

Настоящий автоматизатор должен стремиться к автоматизации всех процессов, которые ему приходится делать руками ©

Вот собственно пока все! Надеюсь, было интересно!
«Стабильных» всем тестов и спасибо за внимание!

Похожие статьи:
У рубриці DOU Проектор всі охочі можуть презентувати свій продукт (як стартап, так і ламповий pet-проект). Якщо вам є про що розповісти —...
Статья написана в соавторстве с Андреем Баулиным, Head of DevOps продуктовой IT-компании Megogo. Разбираемся, почему попытки «внедрения DevOps»...
Два дня интенсивного обучения лидеров изменений и адептов гибкой разработки на основе Scrum, завершаются тестом и получением...
Компанія Alcor, яка спеціалізується на побудові R&D-центрів для ІТ-бізнесів, виходить на ринок Латинської Америки....
Компании Huawei и ABBYY объявили о том, что стали стратегическими партнерами. Среди первых результатов – настройка...
Яндекс.Метрика