Практика скриншот-тестирования получает все большее распространение в сфере андроид-разработки, одним из свидетельств чего можно считать появление всё новых библиотек.
Меня зовут Олег Осипенко и этот цикл статей является развитием моего доклада, посвященного опыту внедрения скриншот-тестирования, который я презентовал на прошедшей недавно в Екатеринбурге конференции DUMP. Отдельную часть доклада я посвятил разбору имеющихся библиотек, но, будучи ограничен временными рамками доклада, я не мог подробно остановиться на их особенностях. И сейчас я хотел бы восполнить это упущение. К тому же уже после доклада я нашел еще 2 новых библиотеки в дополнение к тем 5, что я упоминал в своем выступлении.
В этой статье я расскажу о следующих библиотеках:
Facebook* Screenshot Testing Library
Shot
Paparazzi
Dropshots
Facebook* Screenshot Testing Library
Библиотека от Facebook* появилась еще в 2015 году, когда она была представлена на конференции Droidcon NY. Благодаря этой библиотеке техника скриншот-тестирования заняла свое место в арсенале андроид-разработчиков.
Однако, на данный момент этот проект представляет интерес только с историографической точки зрения. Моя попытка интегрировать эту библиотеку на проекте завершилась неудачей как раз по той причине, что в Android слишком много всего изменилось с момента разработки - Android не стоит на месте, происходит постоянное обновление платформы, появляются новые инструменты и модели работы с данными на устройстве, разрешения и т.д. В этих условиях, любой Android-библиотеке просто для того, чтобы запускаться на новых версиях системы, требуется постоянная поддержка. Про библиотеку от Facebook этого сказать нельзя - её разработка практически остановилась, репозиторий забит открытыми issue и реквестами. К тому же для работы этой библиотеки требуются дополнительные зависимости, так как код для сравнения скриншотов написан на уже официально не поддерживаемом с 2020 года Python 2.
![минусы библиотеки Facebook* минусы библиотеки Facebook*](https://habrastorage.org/getpro/habr/upload_files/586/ebf/724/586ebf7245cc68ae9009360c23dd6d9b.png)
Shot
Развитием данного проекта является Shot, который использует библиотеку от Facebook* под капотом и решает все её проблемы, поскольку проект активно обновлялся вплоть до последнего времени. Интеграция в проект довольно проста: надо только подключить gradle-плагин:
// top-level build.gradle
buildscript {
dependencies {
classpath 'com.karumi:shot:<LATEST_RELEASE>'
}
}
// 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](https://habrastorage.org/getpro/habr/upload_files/f2f/d8f/884/f2fd8f8845d64cb5cf4bdb09fa413fd0.png)
Более подробно об использовании Shot для тестирования дизайн-системы приложения можно узнать из доклада Максима Теймурова на прошлом Mobius.
![достоинства Shot достоинства Shot](https://habrastorage.org/getpro/habr/upload_files/d49/119/08f/d4911908f92e95bd42578716f61d1d7b.png)
Однако, у 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 только для верификации скриншотов:
shot {
runInstrumentation = false
}
В этом случае мы можем запустить инструментальные тесты на Firebase, после этого скачать скриншоты из консоли и запустить их верификацию в Shot.
К тому же Shot требует идентичных настроек эмулятора на машинах, где предполагается выполнение скриншот-тестов, — нужно, чтобы везде использовалась одна и та же модель девайса, с одинаковой плотностью пикселей и т.п. На результат влияет даже разрядность образа эмулятора. Например, использование образа x86_64 вместо x86 приводило к получению отличающихся изображений. Причем отличались они не визуально, а на уровне байтов. Например вот так:
![сравнение байтов двух изображений сравнение байтов двух изображений](https://habrastorage.org/getpro/habr/upload_files/eb7/533/37f/eb753337f431a0f40e92fe8150a3baa2.png)
И это еще одно слабое место Shot - механизм сравнения изображений. Его алгоритм предельно наивен - он просто проходил по байтам двух изображений и сравнивал их на идентичность. Shot предоставляет дополнительную настройку tolerance
- допустимый процент отличающихся пикселей. Но эта настройка слишком грубая: для того, чтобы нейтрализовать эффект из примера выше, tolerance нужно было бы установить на 10-15%, что само по себе бессмысленно - как мы можем говорить о похожести двух изображений, у которых могут отличаться 15% пикселей?! При таком большом значении tolerance
легко пропустить настоящую регрессию.
В последнее время библиотека перестала активно обновляться. По словам автора, раньше он занимался проектом в рамках рабочих обязанностей, но сейчас ситуация изменилась и у него нет возможности посвящать достаточно времени поддержке библиотеки. И хотя у библиотеки есть свое комьюнити и релизы до сих пор появляются, но ждать выпуска какой-то фичи или баг-фикса можно довольно долго. К примеру, когда мы столкнулись с крашем при обработке скриншотов для Compose, выпуска баг-фикса пришлось ждать на протяжении трех месяцев.
![недостатки Shot недостатки Shot](https://habrastorage.org/getpro/habr/upload_files/587/e53/cfa/587e53cfad6438d2894364e2eb3d0931.png)
Paparazzi
Властелины open source из компании Square, библиотеками которых пользуется каждый android разработчик, не прошли мимо скриншот-тестирования, создав новый проект под названием Paparazzi. В отличие от упомянутых выше библиотек Paparazzi не требуется подключение к Android-устройству для создания скриншотов. Вместо этого визуализация компонентов выполняется с использованием LayoutLib — движка рендеринга из Android Studio. Благодаря этому достигается высокая скорость выполнения тестов так как все тесты выполняются на JVM.
![Сравнение скорости выполнения тестов с использованием Shot и PaparazziПо материалам https://jobandtalent.engineering/why-go-with-paparazzi-our-journey-with-android-screenshot-testing-6afa88f41300 Сравнение скорости выполнения тестов с использованием Shot и PaparazziПо материалам https://jobandtalent.engineering/why-go-with-paparazzi-our-journey-with-android-screenshot-testing-6afa88f41300](https://habrastorage.org/getpro/habr/upload_files/6b1/592/c18/6b1592c186091ee18c9df74ff1414381.png)
По материалам https://jobandtalent.engineering/why-go-with-paparazzi-our-journey-with-android-screenshot-testing-6afa88f41300
Для интеграции 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<ConstraintLayout>(R.layout.xml_layout_sample)
paparazzi.snapshot(view)
}
@Test
fun testCompose() {
paparazzi.snapshot {
SampleComposeContent()
}
}
@Test
fun testGif() {
val view = paparazzi.inflate<ConstraintLayout>(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 delta-изображение из Paparazzi](https://habrastorage.org/getpro/habr/upload_files/87a/c30/42a/87ac3042a88354b06c8936c2e3bc15a0.png)
![достоинства Paparazzi достоинства Paparazzi](https://habrastorage.org/getpro/habr/upload_files/84f/208/800/84f20880051f454144cccc9657e855ca.png)
Однако 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 Diff-изображение, созданное во время прогона тестов Dropshots](https://habrastorage.org/getpro/habr/upload_files/131/b82/875/131b828752384ba51565abdd5eb3c2fd.png)
Однако, данный подход наряду с неочевидными преимуществами несет и недостатки: поскольку верификация выполняется на устройстве, время выполнения тестов увеличивается прямо пропорционально размеру изображения. Хотя референсы скриншотов упаковываются в тестовый apk и поэтому уже доступны локально на том устройстве, где выполняются тесты, получение дифф-изображения с устройства происходит с использованием adb, что опять же ограничивает сценарии возможного использования. К примеру, в отличие от Shot, Dropshots возможно использовать в связке с Firebase Test Lab, но в случае возникновения регрессии скачать дифф-изображение будет невозможно, поскольку эта операция производится через adb. Алгоритм верификации, используемый в Dropshots также не отличается особой изощренностью - в наличии два встроенных алгоритма: CountValidator
и ThresholdValidator
. Первый позволяет указать допустимое количество отличающихся пикселей в абсолютном выражении, а второй - их процент. Тем не менее, стоит отметить, что разработчики Dropshots немного позаботились об архитектуре своей библиотеки и оставили возможность использования пользовательских валидаторов, что позволяет реализовать более продвинутый алгоритм сравнения:
@get:Rule
val dropshots = Dropshots(
resultValidator = MyCunnyScreenshotValidator()
)
В следующей части мы рассмотрим еще три библиотеки:
Roborazzi
Testify
Kotlin Snapshot Testing
*Принадлежит meta, признанной в РФ экстремистской организацией.