В первой части мы рассмотрели первую партию библиотек для скриншот-тестирования, сейчас настало время для обзора оставшихся библиотек, среди которых:

  • 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-изображение:

diff-изображение, созданное Roborazzi
diff-изображение, созданное Roborazzi

В целом, идея библиотеки очень интересная, но у неё еще много детских болячек, таких как скудная или отсутствующая документация, местами странный 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 плагин для Android Studio
Testify плагин для Android Studio

С помощью этого плагина еще можно запустить задачи для создания референсного скриншота, загрузки и удаления всех скриншотов с девайса. 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 цвета:

  • красный — для измененных областей изображения;

  • серый — для областей, исключенных из сравнения;

  • желтый — измененные области, но в пределах заявленной погрешности;

  • черный — неизменные области.

В целом это выглядит следующим образом:

High-contrast diff, созданный Testify
High-contrast diff, созданный Testify

В целом стоит отметить, что библиотека 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-изображение:

diff-изображение, созданное Snappy
diff-изображение, созданное Snappy

Функциональность библиотеки в области скриншот-тестов на этом исчерпывается. Само сравнение изображений выполняется на девайсе, но в отличие от Dropshots и Testify, Snappy не пакует референсы в тестовый apk, поэтому перед запуском скриншот-тестирования их надо на девайс загрузить при помощи adb. После прогона тестов опять же при помощи adb нужно скачать diff-изображения или новые референсы. При этом каких-то специальных Gradle-тасков для этого библиотека не предоставляет. Также отсутствует функционал по созданию отчетов о тестировании. К тому же Snappy поддерживает только Compose. В целом, библиотека выглядит как интересная заявка на будущее, особенно с учетом поддержки Mutliplatform, но пока что ей не хватает важных функций для того, чтобы её можно было использовать в своих проектах.


На этом мы завершаем рассмотрение различных библиотек для скриншот-тестирования. В заключительной части мы подведем итоги и сравним рассмотренные библиотеки по различным критериям.

Комментарии (0)