Обзор библиотек для скриншот-тестирования Android проектов. Часть 1
Практика скриншот-тестирования получает все большее распространение в сфере андроид-разработки, одним из свидетельств чего можно считать появление всё новых библиотек.
Меня зовут Олег Осипенко и эта статья является развитием моего доклада, посвященного опыту внедрения скриншот-тестирования, который я презентовал на прошедшей недавно в Екатеринбурге конференции DUMP. Отдельную часть доклада я посвятил разбору имеющихся библиотек, но, будучи ограничен временными рамками доклада, я не мог подробно остановиться на их особенностях. И сейчас я хотел бы восполнить это упущение. К тому же уже после доклада я нашел еще 2 новых библиотеки в дополнение к тем 5, что я упоминал в своем выступлении.
В этой статье я расскажу о следующих библиотеках:
Facebook* Screenshot Testing Library
Библиотека от Facebook* появилась еще в 2015 году, когда она была представлена на конференции Droidcon NY. Благодаря этой библиотеке техника скриншот-тестирования заняла свое место в арсенале андроид-разработчиков.
Однако, на данный момент этот проект представляет интерес только с историографической точки зрения. Моя попытка интегрировать эту библиотеку на проекте завершилась неудачей как раз по той причине, что в Android слишком много всего изменилось с момента разработки — Android не стоит на месте, происходит постоянное обновление платформы, появляются новые инструменты и модели работы с данными на устройстве, разрешения и т.д. В этих условиях, любой Android-библиотеке просто для того, чтобы запускаться на новых версиях системы, требуется постоянная поддержка. Про библиотеку от Facebook этого сказать нельзя — её разработка практически остановилась, репозиторий забит открытыми issue и реквестами. К тому же для работы этой библиотеки требуются дополнительные зависимости, так как код для сравнения скриншотов написан на уже официально не поддерживаемом с 2020 года Python 2.
минусы библиотеки Facebook*
Shot
Развитием данного проекта является Shot, который использует библиотеку от Facebook* под капотом и решает все её проблемы, поскольку проект активно обновлялся вплоть до последнего времени. Интеграция в проект довольно проста: надо только подключить gradle-плагин:
// top-level build.gradle
buildscript {
dependencies {
classpath 'com.karumi:shot:'
}
}
// app build.gradle
apply plugin: 'shot'
а также настроить тестовый раннер:
// app build.gradle
android {
defaultConfig {
testInstrumentationRunner "com.karumi.shot.ShotTestRunner"
}
}
и в проекте появляется несколько новых задач по запуску и обслуживанию скриншот-тестов:
Shot tasks
----------
debugDownloadScreenshots - Retrieves the screenshots stored into the Android device where the tests were executed for the build Debug
debugExecuteScreenshotTests - Checks the user interface screenshot tests . If you execute this task using -Precord param the screenshot will be regenerated for the build Debug
debugRemoveScreenshotsAfter - Removes the screenshots recorded during the tests execution from the Android device where the tests were executed for the build Debug
debugRemoveScreenshotsBefore - Removes the screenshots recorded during the tests execution from the Android device where the tests were executed for the build Debug
executeScreenshotTests - Checks the user interface screenshot tests. If you execute this task using -Precord param the screenshot will be regenerated.
Сами тесты должны реализовать интерфейс ScreenshotTest
, который предоставляет перегруженный метод compareScreenshot()
. Стоит отметить, что библиотека поддерживает не только традиционные view-компоненты, но и Jetpack Compose:
class MyActivityTest: ScreenshotTest {
@Test
fun testActivityIsShownProperly() {
val mainActivity = startMainActivity()
compareScreenshot(activity)
}
}
На выходе получаем вот такой отчет в HTML-формате. Здесь оригинальный
референс, скриншот сделанный во время прогона теста и diff-изображение,
помогающее лучше понять, в чем проблема.
пример отчета о тестировании Shot
Более подробно об использовании Shot для тестирования дизайн-системы приложения можно узнать из доклада Максима Теймурова на прошлом Mobius.
достоинства Shot
Однако, у Shot есть и свои недостатки. Во-первых, Shot для работы требуется доступ к adb. Для создания скриншотов нам требуется запустить тесты на устройстве, даже если мы используем эмулятор, эмулятор работает как отдельная виртуальная машина и у нас нет прямого доступа к его файловой системе. Здесь на помощь приходит adb — Android debug bridge, утилита из состава Android SDK, которая позволяет производить различные манипуляции с подключенным устройством, в том числе получать с него файлы: после прогона тестов Shot с помощью adb скачивает скриншоты и после этого запускает процесс сравнения изображений. Проблема с adb возникает в тот момент, когда над проектом работает несколько десятков человек и CI ежедневно выполняет сотни, если не тысячи проверок. В этот момент у вас может возникнуть непреодолимое желание как-то ускорить этот процесс путем различных оптимизаций, например с использованием отдельной девайс-фермы для запуска инструментальных тестов, такой как Firebase Test Lab. Но подобные фермы устройств, Firebase Test Lab, Aamazon Device Farm и другие, не дают клиентам доступа к adb, что исключает возможность использования Shot.
К тому же Shot требует идентичных настроек эмулятора на машинах, где предполагается выполнение скриншот-тестов, — нужно, чтобы везде использовалась одна и та же модель девайса, с одинаковой плотностью пикселей и т.п. На результат влияет даже разрядность образа эмулятора. Например, использование образа x86_64 вместо x86 приводило к получению отличающихся изображений. Причем отличались они не визуально, а на уровне байтов. Например вот так:
сравнение байтов двух изображений
И это еще одно слабое место Shot — механизм сравнения изображений. Его алгоритм предельно наивен — он просто проходил по байтам двух изображений и сравнивал их на идентичность. Shot предоставляет дополнительную настройку tolerance
— допустимый процент отличающихся пикселей. Но эта настройка слишком грубая: для того, чтобы нейтрализовать эффект из примера выше, tolerance нужно было бы установить на 10–15%, что само по себе бессмысленно — как мы можем говорить о похожести двух изображений, у которых могут отличаться 15% пикселей?! При таком большом значении tolerance
легко пропустить настоящую регрессию.
В последнее время библиотека перестала активно обновляться. По словам автора, раньше он занимался проектом в рамках рабочих обязанностей, но сейчас ситуация изменилась и у него нет возможности посвящать достаточно времени поддержке библиотеки. И хотя у библиотеки есть свое комьюнити и релизы до сих пор появляются, но ждать выпуска какой-то фичи или баг-фикса можно довольно долго. К примеру, когда мы столкнулись с крашем при обработке скриншотов для Compose, выпуска баг-фикса пришлось ждать на протяжении трех месяцев.
недостатки Shot
Paparazzi
Властелины open source из компании Square, библиотеками которых пользуется каждый android разработчик, не прошли мимо скриншот-тестирования, создав новый проект под названием Paparazzi. В отличие от упомянутых выше библиотек Paparazzi не требуется подключение к Android-устройству для создания скриншотов. Вместо этого визуализация компонентов выполняется с использованием LayoutLib — движка рендеринга из Android Studio. Благодаря этому достигается высокая скорость выполнения тестов так как все тесты выполняются на JVM.
Для интеграции Paparazzi нам надо подключить к проекту плагин:
// top-level build.gradle
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.2.0'
}
}
И применить его в модуле: apply plugin: 'app.cash.paparazzi'
. Так как Paparazzi для выполнения тестов парсит ресурсы приложения, его использование возможно только в library-модулях — в модуле application ресурсы представлены уже в бинарном формате, работать с которым библиотека не в состоянии.
Сами тесты создаются в папке src/test и выглядят следующим образом:
class PaparazziScreenshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_2,
theme = "android:Theme.Material.Light.NoActionBar"
)
@Test
fun testXml() {
val view = paparazzi.inflate(R.layout.xml_layout_sample)
paparazzi.snapshot(view)
}
@Test
fun testCompose() {
paparazzi.snapshot {
SampleComposeContent()
}
}
@Test
fun testGif() {
val view = paparazzi.inflate(R.layout.xml_layout_sample)
paparazzi.gif(view)
}
}
Paparazzi поддерживает тестирование как legacy-верстки в XML формате, так макетов, сверстанных с помощью Jetpack Compose и в одном тестовом классе можно комбинировать оба способа. Конфигурация рендеринга выполняется с использованием объекта Paparazzi, позволяющего настроить различные параметры отображения — модель устройства, тема приложения, отображение/скрытие клавиатуры и другие. Кроме того, у Paparazzi есть метод gif()
, с помощью которого мы получим набор фреймов, что удобно для тестирования анимаций:
fun gif(
view: View,
name: String? = null,
start: Long = 0L,
end: Long = 500L,
fps: Int = 30
)
К сожалению, данный метод поддерживает только макеты в XML.
После применения плагина Paparazzi в проекте появляются два новых Gradle-таска — для создания и верификации скриншотов. В случае возникновения регрессии Paparazzi создаст diff-изображение:
delta-изображение из Paparazzi
достоинства Paparazzi
Однако Paparazzi при этом не лишен недостатков. Поскольку Paparazzi использует LayoutLib, его разработчикам требуется какое-то время на интеграцию новой версии LayoutLib из Android Studio в момент появления новой версии Android SDK. Таким образом всегда будет существовать определенный временной лаг между выходом новой версии Android SDK и появлением его поддержки в Paparazzi. По этой же причине Paparazzi очень легко ломается при обновлении Android Gradle Plugin, а также Jetpack Compose. Помимо этого Paparazzi закономерно наследует все баги, которые могут присутствовать в LayoutLib, а также его ограничения — например, если мы используем верстку на XML, мы не можем протестировать скриншот-тестами весь экран сборе, в том случае, если он он сверстан с применением adapter-view, таких как RecyclerView
— на скриншоте мы увидим только RecyclerView, поскольку заполнение его элементами происходит уже во время выполнения приложения при подключении к нему адаптера. Также Paparazzi не поддерживает создание скриншотов из Activity
и Fragment
. Дополнительно стоит отметить свидетельства того, что результаты рендеринга с использованием LayoutLib могут отличаться на различных операционных системах, что будет приводить к падению скриншот-тестов.
Dropshots
Dropshots, еще одна библиотека для скриншот-тестирования ломает сложившуюся схему скриншот-тестирования, когда прогон инструментальных тестов используется для создания скриншотов, а затем запускается их сравнение с образцом. Вместо этого сравнение полученных скриншотов выполняется непосредственно во время выполнения инструментальных тестов. Благодаря этому скриншот-тесты можно запустить кнопкой пуск непосредственно из IDE (при использовании других библиотек необходимо запускать специальный Gradle-таск). Достигается это при помощи выполнения верификации непосредственно на устройстве.
Интеграция Dropshots в проект, наверное, самая простая. Для этого как обычно требуется подключить плагин:
// top-level build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "com.dropbox.dropshots:dropshots-gradle-plugin:0.4.0"
}
}
// build.gradle in the module
apply plugin: "com.dropbox.dropshots"
После этого можно написать новые, либо использовать уже имеющиеся инструментальные тесты — для того, чтобы превратить их в скриншот-тесты всего только нужно добавить вызов ассертов для скриншотов:
@RunWith(AndroidJUnit4ClassRunner::class)
class DropshotsSampleTest {
@gett:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@get:Rule
val dropshots = Dropshots()
@Test
fun testDropshots() {
activityRule.scenario.onActivity {
dropshots.assertSnapshot(it)
}
}
}
После этого можно запустить тесты при помощи стандартной задачи Gradle connectedAndroidTest
, либо нажатием на кнопку пуск в Android Studio. После этого образцы скриншотов будут загружены на девайс, затем на девайсе будут выполнены инструментальные тесты, во время которых будет сделан новый скриншот, запущено его сравнение с образцом и создано diff-изображение в случае возникновения каких-либо ошибок:
Diff-изображение, созданное во время прогона тестов Dropshots
Однако, данный подход наряду с неочевидными преимуществами несет и недостатки: поскольку верификация выполняется на устройстве, время выполнения тестов увеличивается прямо пропорционально размеру изображения. К тому же для выполнения верификации нам необходимо закачать референсные скриншоты на устройство, используемое для тестирования, для чего опять же требуется доступ к adb, что вновь исключает возможность использования Dropshots на Firebase Test Lab. Алгоритм верификации, используемый в Dropshots также не отличается особой изощренностью — в наличии два встроенных алгоритма: CountValidator
и ThresholdValidator
. Первый позволяет указать допустимое количество отличающихся пикселей в абсолютном выражении, а второй — их процент. Стоит отметить, что разработчики Dropshots немного позаботились об архитектуре своей библиотеки и оставили возможность использования пользовательских валидаторов, что позволяет реализовать более продвинутый алгоритм сравнения:
@get:Rule
val dropshots = Dropshots(
resultValidator = MyCunnyScreenshotValidator()
)
В следующей части мы рассмотрим еще три библиотеки:
Roborazzi
Testify
Kotlin Snapshot Testing
*Принадлежит meta, признанной в РФ экстремистской организацией