В первой части мы рассмотрели первую партию библиотек для скриншот-тестирования, сейчас настало время для обзора оставшихся библиотек, среди которых:
Roborazzi
Android Testify
Snappy (Kotlin snapshot testing)
Roborazzi
Следующей в нашем списке идет библиотека Roborazzi. Это интересная попытка с новой стороны подойти к реализации той же идеи, что была заложена в Paparazzi - ускорить скриншот-тестирование путем запуска тестов не на устройствах Android, а на JVM. Здесь для создания скриншотов используется Robolectric, в котором с недавних пор появился новый графический режим - Native. Robolectric поставляется с набором моков для классов из Android SDK, но с новым режимом графический код будет выполняться на реальных классах из Android OS. Благодаря этому мы получаем преимущества Paparazzi в виде высокой скорости выполнения тестов, но без его недостатков. Так, поскольку библиотека не завязывается на парсинг ресурсов, Roborazzi может использоваться не только в библиотечных модулях, но и в application-модуле. Помимо этого отсутствует ограничение на создание скриншотов всей activity
, так как захват скриншотов происходит непосредственно с "экрана".
Интеграция библиотеки в проект как водится начинается с подключения Gradle-плагина и необходимых зависимостей:
//top-level build.gradle
buildscript {
dependencies {
classpath "io.github.takahirom.roborazzi:roborazzi-gradle-plugin:[version]"
}
}
//module build.gradle]
testImplementation("io.github.takahirom.roborazzi:roborazzi:[version]")
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:[version]")
testImplementation("io.github.takahirom.roborazzi:roborazzi-junit-rule:[version]")
// также необходимо установить Robolectric
testImplementation("org.robolectric:robolectric:4.10.3")
В самом тесте необходимо включить поддержку нативной графики:
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class RoborazziTest
Roborazzi предоставляет два варианта написания тестов. При использовании RoborazziRule
скриншоты для тестов будут созданы автоматически:
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class RoborazziScreenshotTest {
@get:Rule(order = 0)
val composeRule = createAndroidComposeRule<ComponentActivity>()
@get:Rule(order = 1)
val roborazzi = RoborazziRule(
composeRule,
composeRule.onRoot()
)
@Test
fun testComposable() {
composeRule.setContent {
SampleComposeContent()
}
}
}
Либо можно вручную вызывать метод captureRoboImage()
для создания скриншотов:
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class RoborazziTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testRoborazzi() {
activityRule.scenario.onActivity {
it.findViewById<View>(android.R.id.content)
.captureRoboImage("src/test/snapshots/images/test.png")
}
}
}
Для запуска тестов мы используем gradle-таски, предоставляемые библиотекой:
Roborazzi tasks
---------------
recordRoborazziDebug - Record a screenshot
compareRoborazziDebug - Review changes made to an image. This action will compare the current image with the saved one, generating a comparison image labeled as [original]_compare.png. It also produces a JSON file containing the diff information, which can be found under build/test-results/roborazzi.
verifyRoborazziDebug - Validate changes made to an image. If there is any difference between the current image and the saved one, the test will fail.
verifyAndRecordRoborazziDebug - This task will first verify the images and, if differences are detected, it will record a new baseline.
Основная разница между compareRoborazziDebug
и verifyRoborazziDebug
заключается в том, что verifyRoborazziDebug
бросит исключение в том случае, если скриншот изменился, а compareRoborazziDebug
просто произведет сравнение двух изображений.
В случае расхождения между полученным скриншотом и референсом Roborazzi создаст diff-изображение:
В целом, идея библиотеки очень интересная, но у неё еще много детских болячек, таких как скудная или отсутствующая документация, местами странный API и спорные архитектурные решения. Например, файлы из текущего прогона и diff-изображения, которые по сути являются временными файлами, создаются внутри папки с референсами вместо того, чтобы использовать для них папку build
. Но библиотека активно развивается, что дает надежду на решение этих проблем. Использование Robolectric хотя и позволяет выйти за рамки ограничений, присущих Paparazzi, но приносит свои проблемы типичные для Robolectric. Также стоит отметить, что Roborazzi использует для сравнения изображений библиотеку Differ от Dropbox, которая используется в рассмотренной нами в первой части библиотеке Dropshots.
Android Testify
Разработка этой библиотеки стартовала в компании Shopify еще в конце 2018 года, однако сейчас продолжается в отдельном репозитории. Testify работает по той же схеме, что и Dropshots - сравнение изображений выполняется непосредственно на устройстве во время прогона тестов. Для этого референсные изображения сохраняются в папке androidTest/assets
, что позволяет упаковать их в тестовый apk и запустить сравнение локально.
Интеграция в проект требует подключения Gradle-плагина:
// top-level build.gradle
classpath "dev.testify:plugin:[version]"
// module build.gradle
plugins {
id("dev.testify")
}
В самих тестах вместо обычного ActivityScenarioRule
следует использовать ScreenshotRule
, тестовый метод необходимо пометить аннотацией @ScreenshotInstrumentation
и затем вызвать метод assertSame
:
@RunWith(AndroidJUnit4::class)
class ExampleTestifyTest {
@get:Rule
val screenshotRule = ScreenshotRule(MainActivity::class.java)
@ScreenshotInstrumentation
@Test
fun testify() {
screenshotRule.assertSame()
}
}
Для запуска тестов мы можем использовать Gradle-task screenshotTest
, но также сгодится и стандартный таск Android connectedCheck
. Помимо этого у Testify есть свой плагин для Android Studio, с которым запуск теста возможен непосредственно из сайдбара в IDE:
С помощью этого плагина еще можно запустить задачи для создания референсного скриншота, загрузки и удаления всех скриншотов с девайса. Testify предоставляет несколько групп Gradle-тасков:
Testify tasks
-------------
screenshotClear - Remove any existing screenshot test images from the device
screenshotPull - Pull screenshots from the device and wait for all files to be committed to disk
screenshotRecord - Run the screenshot tests and record a new baseline
screenshotTest - Run the Testify screenshot tests
Testify reports tasks
---------------------
reportPull - Pull report.yml from the device and wait for it to be committed to disk
reportShow - Print the test result report to the console
Testify utility tasks
---------------------
deviceLocale - Displays the device locale.
deviceTimeZone - Displays the time zone currently set on the device
disableSoftKeyboard - Disables the soft keyboard on the device
hidePasswords - Hides passwords fully on the device
testifyDevices - Displays Testify devices
testifyKey - Displays the Testify output key for the current device
testifySettings - Displays the Testify gradle extension settings
testifyVersion - Displays the Testify plugin version
По умолчанию в Testify отключена генерация diff-изображений, но если мы хотим её включить, то это можно сделать одним из трех способов. Либо в глобально для всех тестов в манифесте:
<application>
<meta-data android:name="testify-generate-diffs" android:value="true" />
</application>
Для всего тестового класса:
@ScreenshotInstrumentation
@Test
fun test() {
TestifyFeatures.GenerateDiffs.setEnabled(true)
rule.assertSame()
}
Или же для одного тестового метода при помощи ScreenshotRule
:
@ScreenshotInstrumentation
@Test
fun testDefault() {
rule
.withExperimentalFeatureEnabled(TestifyFeatures.GenerateDiffs)
.assertSame()
}
В этом случае при возникновении регрессии Testify создаст так называемый High‑Contrast diff, где каждое изменение, неважно насколько значительное, будет отмечено красным цветом. В общей сложности diff‑изображение может содержать 4 цвета:
красный — для измененных областей изображения;
серый — для областей, исключенных из сравнения;
желтый — измененные области, но в пределах заявленной погрешности;
черный — неизменные области.
В целом это выглядит следующим образом:
В целом стоит отметить, что библиотека Testify лично мне кажется наиболее проработанной и гибкой, поддерживающей различные сценарии тестирования и решающей различные проблемы, с которыми сталкивается каждый, кому приходится заниматься таким неблагодарным делом как инструментальное тестирование Android. Так, в отличие от других библиотек Testify создает отдельную папку для каждой конфигурации тестового устройства, например:
Кроме этого ScreenshotRule
позволяет передавать аргументы для активити, в которой будут запускаться тесты:
@ScreenshotInstrumentation
@Test
fun default() {
rule
.addIntentExtras {
it.putString("TOOLBAR_TITLE", "addIntentExtras")
}
.assertSame()
}
А также произвести какие-то манипуляции перед тем, как произвести скриншот-тестирование:
@ScreenshotInstrumentation
@Test
fun setEspressoActions() {
rule
.setEspressoActions {
onView(withId(R.id.edit_text)).perform(typeText("Testify"))
}
.assertSame()
}
Или же остановить выполнение тестов для анализа верстки при помощи метода setLayoutInspectionModeEnabled(true)
. Кроме того ScreenshotRule
дает возможность поменять в тесте локаль, масштабирование шрифтов, ориентацию экрана, либо запросить фокус на каком-либо элементе экрана. По умолчанию Testify захватывает весь экран при создании скриншота, но также возможно создание скриншота только для какого-то определенного View или же исключение области экрана из сравнения:
@ScreenshotInstrumentation
@Test
fun default() {
rule
.defineExclusionRects { rootView, exclusionRects ->
val card = rootView.findViewById<View>(R.id.info_card)
exclusionRects.add(card.boundingBox)
}
.assertSame()
}
Также разработчики предоставляют возможность кастомизировать метод для создания скриншотов и их рендеринг. По умолчанию в Android используется аппаратное ускорение для рендеринга интерфейса, в результате чего мы можем получить определенным образом различающиеся скриншоты при использовании различных устройств. На результат влияют как различия в CPU, так и GPU, тип видеокарты (дискретная или встроенная) и другие факторы. Отличия могут возникнуть в следующих участках:
антиалиасинг и сглаживание шрифтов;
тени и
elevation
;области с прозрачностью.
Для того, чтобы нивелировать подобные различия существует несколько способов, самым простым из которых является использование одной и той же конфигурации для тестирования. Однако зачастую это невыполнимая задача: внутри одной команды разработчики могут вести разработку на ЭВМ, использующих различные процессорные архитектуры, видеокарты, использование различных CI сервисов также вносит свою долю неопределенности. Другим вариантом является отключение аппаратного ускорения для тестов. Testify позволяет отключать аппаратное ускорение для определенных тестов:
@ScreenshotInstrumentation
@Test
fun default() {
rule
.setUseSoftwareRenderer(true)
.assertSame()
}
Стоит упомянуть о том, что Testify дает возможность использовать различные методы для создания скриншотов. По умолчанию из соображений обратной совместимости используется метод View.getDrawingCache()
, но возможно использование рендеринга на Canvas
при помощи метода View.draw()
, однако наиболее предпочтительным является использование API PixelCopy
, поскольку в этом случае скриншот создается из Surface, что позволяет корректно сохранить в скриншоте элементы, которые зависят от аппаратного ускорения. Включить нужный метод создания скриншота с использованием способами, аналогичными включению генерации diff-изображений, с с использованием соответствующих TestifyFeatures
:
CanvasCapture
PixelCopyCapture
Помимо этого Testify дает возможность игнорировать небольшие различия в изображениях с использованием более продвинутых алгоритмов сравнения, чем простой подсчет количества отличающихся пикселей. Так Testify позволяет считать для скриншотов параметр Дельта E, использование которого является стандартом в цветокоррекции при сравнении двух изображений. Слово "дельта" является стандартным обозначением для различия какой-либо величины. Буква "Е" — это первая буква немецкого слова Empfindung, "ощущение". Дословно понятие обозначает разницу в ощущениях. Testify использует формулу CIE LAB 94 для вычисления Дельта Е
Кроме того, Testify предоставляет несколько расширений. Во-первых, это расширение для Compose. Во-вторых, это расширение Accessibility Сhecks, которое добавляет еще один метод для ScreenshotRule
- assertAccessibility()
. Вызов этого метода будет выполнять набор проверок из Accessibility Test Framework.
Также благодаря выполнению тестов на девайсе Testify можно использовать с Firebase Test Lab: референсы скриншотов упаковываются в тестовый apk и доступны на устройстве. Также решена проблема со скачиванием референсов и diff-изображений с девайса. Если Dropshots использует для этого adb, который не работает на FTL, то Testify специально для таких случаев позволяет использовать SD Card для записи скриншотов и diff-изображений. Благодаря этому скриншоты, вместе с остальными тестовыми артефактами будут сохранены в Google cloud, откуда их можно скачать вручную, либо, при использовании Flank, автоматически.
В целом, несмотря на отдельные недочеты, такие как местами устаревшая документация или неработающие фичи (например, в текущей бете поломан таск по скачиванию отчетов о тестировании, но фикс уже в пути) библиотека Testify выглядит как тот самый грааль скриншот-тестирования. Единственное, чего на мой взгляд не хватает этой библиотеке — это поддержки Gradle managed devices.
Snappy (Kotlin snapshot testing)
Последней в нашем списке идет небольшая библиотека Snappy. На данный момент вряд ли имеет смысл затаскивать её в проект — слишком сырой она выглядит. Библиотека написана с использованием Kotlin Mutliplatform, поэтому для того, чтобы её испытать в проект надо подключить несколько зависимостей:
// top-level build.gradle
allprojects {
repositories {
maven { url = uri("https://jitpack.io") }
}
}
// module build.gradle
androidTestImplementation("com.github.quickbirdstudios.kotlin-snapshot-testing:snapshot:[version]")
androidTestImplementation("com.github.QuickBirdEng.kotlin-snapshot-testing:snapshot-android:[version]")
Библиотека еще не имеет своего артефакта на mavenCentral
и распространяется через Jitpack. Тестовый класс нужно наследовать от AndroidFileSnapshotTest
, который предоставляет метод snapshotToFilesDir()
. Поскольку метод объявлен как , вызывать его необходимо с использованием вспомогательного метода runTest()
:
@RunWith(AndroidJUnit4ClassRunner::class)
class SampleSnappyTest : AndroidFileSnapshotTest() {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun testScreenshot() {
runTest {
FileSnapshotting.composeScreenshot
.snapshotToFilesDir(composeTestRule)
}
}
}
Метод создаст новый скриншот и запустит его сравнение с образцом. В случае, если образца нет, при помощи параметра record = true
можно создать референс. Отдельной задачи на запуск скриншот-тестов нет, тесты выполняются в рамках стандартного запуска инструментальных тестов. В случае регрессии будет создано diff-изображение:
Функциональность библиотеки в области скриншот-тестов на этом исчерпывается. Само сравнение изображений выполняется на девайсе, но в отличие от Dropshots и Testify, Snappy не пакует референсы в тестовый apk, поэтому перед запуском скриншот-тестирования их надо на девайс загрузить при помощи adb. После прогона тестов опять же при помощи adb нужно скачать diff-изображения или новые референсы. При этом каких-то специальных Gradle-тасков для этого библиотека не предоставляет. Также отсутствует функционал по созданию отчетов о тестировании. К тому же Snappy поддерживает только Compose. В целом, библиотека выглядит как интересная заявка на будущее, особенно с учетом поддержки Mutliplatform, но пока что ей не хватает важных функций для того, чтобы её можно было использовать в своих проектах.
На этом мы завершаем рассмотрение различных библиотек для скриншот-тестирования. В заключительной части мы подведем итоги и сравним рассмотренные библиотеки по различным критериям.