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

Меня зовут Алексей Лакович, я занимаюсь автоматизацией тестирования в компании Генезис. В тестировании я работаю уже более 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 и скриншоты — такой отчет уже будет очень полезен :)

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

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

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

Похожие статьи:
Ранее стало известно, что компания Samsung может сдвинуть презентацию своего смартфона Galaxy S7 на январь 2016 года, а теперь появились...
EdTech-стартап Mate academy безплатно навчатиме ІТ-спеціальностей ветеранів та ветеранок. Серед пропонованих курсів — Front-end developer,...
Сервис электронной почты Gmail от Google стал еще одним сервисом компании, число активных пользователей которого превысило...
Wedding silk sarees will never out of style from Indian wedding and it’s again returned to the design pattern. Amid going to of many wedding we have seen that numerous ladies’ wore silk Kanchipuram wedding...
У Вашей профессии нет перспектив, и Вы хотите изменить свою жизнь, перейдя в IT-сферу? Тогда курс...
Яндекс.Метрика