Практика скриншот-тестирования получает все большее распространение в сфере андроид-разработки, одним из свидетельств чего можно считать появление всё новых библиотек. 

Меня зовут Олег Осипенко и эта статья является развитием моего доклада, посвященного опыту внедрения скриншот-тестирования, который я презентовал на прошедшей недавно в Екатеринбурге конференции 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*

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

Более подробно об использовании Shot для тестирования дизайн-системы приложения можно узнать из доклада Максима Теймурова на прошлом Mobius.

достоинства Shot
достоинства 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 только для верификации скриншотов:

shot {
    runInstrumentation = false
}

В этом случае мы можем запустить инструментальные тесты на Firebase, после этого скачать скриншоты из консоли и запустить их верификацию в Shot.

К тому же Shot требует идентичных настроек эмулятора на машинах, где предполагается выполнение скриншот-тестов, — нужно, чтобы везде использовалась одна и та же модель девайса, с одинаковой плотностью пикселей и т.п. На результат влияет даже разрядность образа эмулятора. Например, использование образа x86_64 вместо x86 приводило к получению отличающихся изображений. Причем отличались они не визуально, а на уровне байтов. Например вот так:

сравнение байтов двух изображений
сравнение байтов двух изображений

И это еще одно слабое место Shot - механизм сравнения изображений. Его алгоритм предельно наивен - он просто проходил по байтам двух изображений и сравнивал их на идентичность. Shot предоставляет дополнительную настройку tolerance - допустимый процент отличающихся пикселей. Но эта настройка слишком грубая: для того, чтобы нейтрализовать эффект из примера выше, tolerance нужно было бы установить на 10-15%, что само по себе бессмысленно - как мы можем говорить о похожести двух изображений, у которых могут отличаться 15% пикселей?! При таком большом значении tolerance легко пропустить настоящую регрессию.

В последнее время библиотека перестала активно обновляться. По словам автора, раньше он занимался проектом в рамках рабочих обязанностей, но сейчас ситуация изменилась и у него нет возможности посвящать достаточно времени поддержке библиотеки. И хотя у библиотеки есть свое комьюнити и релизы до сих пор появляются, но ждать выпуска какой-то фичи или баг-фикса можно довольно долго. К примеру, когда мы столкнулись с крашем при обработке скриншотов для Compose, выпуска баг-фикса пришлось ждать на протяжении трех месяцев.

недостатки Shot
недостатки Shot

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

Для интеграции 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
достоинства 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
Diff-изображение, созданное во время прогона тестов Dropshots

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

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


  1. Phansier
    29.06.2023 15:24

    Можно во вторую часть добавить еще Kaspresso https://kasperskylab.github.io/Kaspresso/Wiki/Screenshot_tests/


    1. basnopisets Автор
      29.06.2023 15:24
      +1

      Спасибо за рекомендацию. Насколько я понимаю, Kaspresso не содержит никакой инфраструктуры для проведения именно скриншот-тестирования. По приведенной вами ссылке только инструкция о том, как создать скриншот с помощью Kaspresso, однако скриншот-тестирование подразумевает еще и верификацию полученных скриншотов, путем сравнения их с заранее подготовленным референсом. И вот этой части в Kaspresso как раз нет.
      Можно упомянуть, конечно, но по сути эта функциональность предоставляется как UIAutomator, так и Espresso.