Изначально Appium создавался для автоматизации мобильных приложений на iOS и Android, со временем превратился в полнофункциональную платформу, автоматизирующую через WebDriver (для Windows-приложений).
Начиная с версии 2.0, каждый драйвер изолирован от серверных приложений Appium и может управляться самостоятельно с помощью интерфейса командной строки Appium Driver.
Что такое визуальное тестирование
Важнейший вид тестирования, который может выявлять как визуальные, так и функциональные дефекты. Хотя в целом оно дополняет функциональное тестирование, визуальное тестирование является самодостаточным.
- Главная задача: оценивает визуальные изменения, гарантируя, что на прод передаются только нужные изменения в интерфейсе.
- Во многих компаниях полагают, что при проведении тщательного функционального тестирования визуальное тестирование не требуется. Это ошибка.
- Потому что визуальное тестирование находит дефекты, которые функциональное тестирование не замечает, и гарантирует, что приложение выглядит так, как должно выглядеть.
- Визуальное регрессионное — когда нужно найти сложные дефекты, пропущенные функциональным сьютом.
Важность UX
Почему придается такое большое значение пользовательскому опыту (UX, User Experience)?
- Потому что независимо от того, насколько хороша основная функциональность приложения, она будет бесполезной для пользователя, если не обеспечен хороший пользовательский опыт, удобный приятный интерфейс, привлекательный визуальный дизайн.
- Автоматизация визуальных тестов экономит средства компании, улучшая ее долгосрочную экономическую эффективность; визуальные автотесты разумеется быстрее чем ручные; в целом более точны, поскольку не опираются на человеческий фактор; визуальные автотесты — «пиксельно идеальные» (pixel-perfect).
- Они многократно используются (реюзабельные) и их результаты понятны другим членам команды (в автоматически создаваемых репортах).
Учитывая все это, команды должны уделять визуальному тестированию значительное время и ресурсы. В сочетании с юнит-тестами визуальные тесты помогут обнаружить визуальные дефекты на ранних этапах жизненного цикла разработки.
Библиотека OpenCV
Мы знаем, как делать скриншоты в Appium. А как сравнить два скриншота, чтобы обнаружить визуальные различия?
Базовый подход к визуальному сравнению будет неправильным, потому что даже алгоритмы обработки изображений могут добавить различия.
Но существует библиотека компьютерного зрения OpenCV с функциями для работы с изображениями, которую в данном случае можно использовать без необходимости вникать в тонкости.
В Appium есть поддержка OpenCV, но она не включена по умолчанию, поскольку OpenCV и ее связки с Node.js — требовательны к ресурсам и должны настраиваться на каждой платформе независимо.
Чтобы подготовить библиотеку для использования в Appium, выполните команду npm install -g opencv4nodejs
.
Шаги визуальной валидации
Цель: снять скриншоты каждого экрана (view), с которым мы взаимодействуем в приложении (при функциональном тестировании). Мы сравниваем каждый скриншот с предыдущим скриншотом того же экрана, с помощью приложения для анализа изображений.
В процессе проверки мы можем обнаружить визуальную регрессию, то есть если возникнут значительные нежелательные изменения. В этот момент мы можем либо прекратить выполнение теста, либо зафиксировать этот дефект в базу данных, для дальнейшей оценки командой.
- Шаг 1: На этом этапе нам нужно протестировать приложение и сделать скриншоты.
- Шаг 2: На этом этапе скриншоты сравниваются с базовыми (baseline) скриншотами при помощи инструмента автоматизации (в нашем случае Appium). Как правило, базовые скриншоты — это скриншоты, сделанные во время предыдущих сессий тестирования.
- Шаг 3: После получения результатов сравнения изображений — приложение генерирует отчет (репорт) с описанием замеченных несоответствий (отличий) между изображениями.
- Шаг 4: На последнем этапе тестировщик просматривает репорт, определяя, является ли каждое найденное отличие дефектом (чтобы исключить ложные срабатывания). Далее baseline-изображения обновляются.
Так как это первый наш практикум, у нас еще нет базовых изображений. Поэтому в качестве базовых будут использованы изображения из первого тестового прогона. Они будут сравниваться со скриншотами, начиная со следующего прогона.
Пример визуального сравнения
Будем проверять наличие знака «$» на этом изображении. Сначала делаем скриншот пользовательского интерфейса, затем в OpenCV сравниваем изображения, и если они не совпадают, то передаем баг на проверку.

Настройка тестового окружения
Установка Appium, ASDK и Java
Скачайте и установите Java (JDK) и пропишите пути к папке JDK и bin.
- Скачайте файл «.exe» отсюда (версия: jdk1.8.0_91 или любая другая последняя).
- Установите .exe-файл.
- Пропишите путь к папке JDK bin в переменной окружения вашей системы.
Загрузка Android SDK
- Найдите по ссылке «android-sdk_r24.4.1-windows.zip» (или более свежей версии) и загрузите.
- После загрузки zip-файла распакуйте его в папку.
- Запустите файл «SDK Manager.exe«.
- Откроется окно Android SDK Manager. Выберите «Tools» и Android-платформу, на которой будете тестировать.
Установка Appium
- Откройте официальный сайт Appium.
- Нажмите «Скачать».
- Выберите ОС, в которой вы работаете, и загрузите соответствующую версию.
- Распакуйте загруженную zip-папку.
- Установите .exe-файл «appium-installer«.
Настройка Appium для визуального тестирования
Appium поддерживает OpenCV, но ее надо отдельно включать, поскольку разработка OpenCV и его привязок к Node.js должна выполняться на производительных машинах.
- Самый простой способ подготовить все к работе — командой
npm install -g opencv4nodejs
. - Система попытается установить Node-привязки глобально, а также загрузить и собрать OpenCV на вашей машине.
- Если это не получится, можете установить OpenCV через Homebrew, а затем установить Node-привязки с флагом окружения
OPENCV4NODEJS_DISABLE_AUTOBUILD=1
, чтобы заставить систему использовать установленные в ней бинарники.
После установки пакета opencv4nodejs
необходимо убедиться, что он доступен Appium при запуске. Один из способов — выполнить команду npm install без параметра -g
в каталоге Appium. Другой вариант — добавить глобальную папку node_modules
в переменную окружения NODE_PATH
.
Практикум
Тест-кейс
Выполните следующие шаги:
- Сначала сохраняем изображение главной страницы приложения в качестве базового (baseline) изображения.
- Запускаем приложение с помощью cap-файла appium.
- Ожидаем, пока загрузится главная страница.
- Нажимаем кнопку “Добавить товар в корзину”
- Нажимаем кнопку “Корзина”
- Заходим в корзину и проверяем экран корзины с базовым изображением (ScreenShot)

! При первом запуске он будет таким же, как и исходные изображения по заданному пути. При втором выполнении он возвращает пороговое значение совпадения (match threshold value), которое мы уже установили в нашем коде.
Дизайн фреймворка и код
Структура фреймворка:

Тестовый сценарий
AppiumVisualTestBrowserStackAPP.java import io.appium.java_client.MobileBy; import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; import io.appium.java_client.imagecomparison.SimilarityMatchingResult; import java.io.File; import java.net.URISyntaxException; import org.apache.commons.io.FileUtils; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.OutputType; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; public class AppiumVisualTestBrowserStackAPP extends BaseTest { // Give a file path where we can save the matched file private final static String path_to_validate = "/Users/Download/bs_demo/"; private final static String CHECK_HOME = "home_screen"; private final static String CART_PAGE = "cart_page"; private final static String BASEIMAGE = "BASEIMAGE_"; private final static double Breakpoint_for_Match = 0.99; //Thresold Value private final static By ADD_TO_CART = MobileBy.AccessibilityId("add-to-cart-12"); private final static By NAV_TO_CART = MobileBy.AccessibilityId("nav-cart"); @Override protected DesiredCapabilities getCaps() throws URISyntaxException { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability("platformName", "Android"); capabilities.setCapability("deviceName", "Android Emulator"); capabilities.setCapability("automationName", "UiAutomator2"); capabilities.setCapability("app", getResource("apps/browserstack-demoapp.apk").toString()); //Make sure we uninstall the app before each test regardless of version capabilities.setCapability("uninstallOtherPackages", "io.cloudgrey.the_app"); return capabilities; } private WebElement waitForElement(WebDriverWait wait, By selector) { WebElement el = wait.until(ExpectedConditions.presenceOfElementLocated(selector)); try { Thread.sleep(750); } catch (InterruptedException ign) {} return el; } @Test public void testAppDesign() throws Exception { WebDriverWait wait = new WebDriverWait(driver, 5); // wait for an element that's on the home screen WebElement addToCart = waitForElement(wait, ADD_TO_CART); // now we know the home screen is loaded, so do a visual check doVisualCheck(CHECK_HOME); // Click on add to cart btn for adding item in card addToCart.click(); WebElement navToCart = waitForElement(wait, NAV_TO_CART); //click to cart btn navToCart.click(); //Perform our second visual check, this time of the cart page doVisualCheck(CART_PAGE); } private void doVisualCheck(String checkName) throws Exception { String basematchFilename = path_to_validate + "/" + BASEIMAGE + checkName + ".png"; File basematchImg = new File(basematchFilename); // If there is no basematch picture for this check, one should be made. if (!basematchImg.exists()) { System.out.println(String.format("No basematch found for '%s' check; capturing baseline instead of checking", checkName)); File newBasematch = driver.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(newBasematch, new File(basematchFilename)); return; } // Otherwise, obtain the picture similarity from Appium if we discover a basematch. Obtaining the resemblance // We also enable visualisation so that, should something go wrong, we can see what went wrong. SimilarityMatchingOptions opts = new SimilarityMatchingOptions(); opts.withEnabledVisualization(); SimilarityMatchingResult res = driver.getImagesSimilarity(basematchImg, driver.getScreenshotAs(OutputType.FILE), opts); // If the similarity is not high enough, consider the check to have failed if (res.getScore() < Breakpoint_for_Match) { File failViz = new File(path_to_validate + "/FAIL_" + checkName + ".png"); res.storeVisualization(failViz); throw new Exception( String.format("Visual check of '%s' failed; similarity match was only %f, and below the breakPoint of %f. Visualization written to %s.", checkName, res.getScore(), Breakpoint_for_Match , failViz.getAbsolutePath())); } // Otherwise, it passed! System.out.println(String.format("Visual check of '%s' passed; similarity match was %f", checkName, res.getScore())); } }
BaseTest.java
import io.appium.java_client.android.AndroidDriver; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import org.junit.After; import org.junit.Before; import org.openqa.selenium.remote.DesiredCapabilities; public class BaseTest { AndroidDriver driver; protected DesiredCapabilities getCaps() throws Exception { throw new Exception("Must use getCaps"); } @Before public void setUp() throws Exception { URL server_url = new URL("http://localhost:4723/wd/hub"); driver = new AndroidDriver(server_url, getCaps()); } @After public void tearDown() { if (driver != null) { driver.quit(); } } Path getResource(String file_name) throws URISyntaxException { URL ref_img_url = getClass().getClassLoader().getResource(file_name); return Paths.get(ref_img_url .toURI()).toFile().toPath(); } private String getResourceB64(String file_name) throws URISyntaxException, IOException { Path ref_img_path = getResource(file_name); return Base64.getEncoder().encodeToString(Files.readAllBytes(ref_img_path )); } String getReferenceImageB64(String file_name) throws URISyntaxException, IOException { return getResourceB64("images/" + file_name); } }
Вы можете запустить этот скрипт в main-классе.
Анализ результатов
После первого запуска мы сохранили изображения как «базовые» — главной страницы приложения и экран после добавления товаров в корзину.

- Мы добавили в код пороговое значение для сравнения изображений.
- Если фактическое значение
getScore
превышает пороговое значение, тест-кейс красный. - Если значение
getScore
меньше порогового, то тест пройдет.
private final static double Breakpoint_for_Match= 0.99; if (res.getScore() < Breakpoint_for_Match) { File failViz = new File(path_to_validate + "/FAIL_" + checkName + ".png"); res.storeVisualization(failViz); throw new Exception( String.format("Visual check of '%s' failed; similarity match was only %f, and below the Breakpoint_for_Match of %f. Visualization written to %s.", checkName, res.getScore(), Breakpoint_for_Match, failViz.getAbsolutePath()));} // Otherwise, it passed! System.out.println(String.format("Visual check of '%s' passed; similarity match was %f", checkName, res.getScore()));
Команды сравнения изображений
При запуске этой команды с соответствующими байтовыми массивами изображений (в данном примере img1
— это «базовая линия», а img2
— снепшот, который мы хотим проанализировать) и объектом опций (он должен иметь версию SimilarityMatchingOptions
), создается объект SimilarityMatchingResult
.
- Самая важная функция объекта
result
—getScore
, которая покажет, насколько разнятся два имеющихся снепшота, от 0 до 1. - Мы оцениваем, превышает ли этот балл ранее заданный порог, который выбирается в зависимости от особенностей приложения.
- Если превышает порог, то считаем, что различие большое, и создаем исключение, или засчитываем дефект.
SimilarityMatchingResult res = driver.getImagesSimilarity(baselineImg, driver.getScreenshotAs(OutputType.FILE), opts);
Советы и лучшие практики
- Инструмент должен обрабатывать сглаживание (anti-aliasing), сдвиг пикселей и т. д.
- Процесс можно ускорить благодаря технике моментальных снимков DOM (DOM snapshotting) и расширенным функциям параллелизации, предназначенным для масштабированного запуска сложных тестовых наборов
- Чаще обсуждайте снепшоты, информируйте остальных членов команды о продвижении процесса.
- Существуют инструмент наподобие Percy, который умеют игнорировать ложные срабатывания при визуальном тестировании.
- Автоматизация процессов должна учитывать изменения условий в проекте.
- Не опирайтесь только на числа — пороговые значения и количество ошибок. Единственное, что важно — заметит ли реальный пользователь разницу и повлияет ли она на взаимодействие с продуктом.
- Автоматизация должна уметь оценивать структуру страницы и сравнивать макеты.
- Тестируйте весь интерфейс страницы, а не отдельные части. Это обеспечит полный охват. Вы рискуете пропустить нестандартные дефекты, если будете проверять только отдельные компоненты.
- Работайте в фреймворке по возможности с качественными изображениями, чтобы корректно обрабатывать пороговые значения.