Изначально 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(Exp壯陽藥
ectedConditions.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, который умеют игнорировать ложные срабатывания при визуальном тестировании.
- Автоматизация процессов должна учитывать изменения условий в проекте.
- Не опирайтесь только на числа — пороговые значения и количество ошибок. Единственное, что важно — заметит ли реальный пользователь разницу и повлияет ли она на взаимодействие с продуктом.
- Автоматизация должна уметь оценивать структуру страницы и сравнивать макеты.
- Тестируйте весь интерфейс страницы, а не отдельные части. Это обеспечит полный охват. Вы рискуете пропустить нестандартные дефекты, если будете проверять только отдельные компоненты.
- Работайте в фреймворке по возможности с качественными изображениями, чтобы корректно обрабатывать пороговые значения.