Как заставить тесты «видеть» ошибки: элегантный способ автоматизации тестирования
Меня зовут Алексей Лакович, я занимаюсь автоматизацией тестирования в компании Генезис. В тестировании я работаю уже более 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 и скриншоты — такой отчет уже будет очень полезен :)
Ну и засыпайте всех алертами на почту — это стимулирует фиксить тесты и баги :)
Настоящий автоматизатор должен стремиться к автоматизации всех процессов, которые ему приходится делать руками ©
Вот собственно пока все! Надеюсь, было интересно!
«Стабильных» всем тестов и спасибо за внимание!