Как стать автором
Обновить
1047.95
OTUS
Цифровые навыки от ведущих экспертов

Найдите 10 отличий. Тестируем сравнением снимков экрана с образцом

Время на прочтение5 мин
Количество просмотров2.6K

При тестировании мобильных приложений нередко возникает необходимость проверить корректность верстки визуальных элементов и их правильное отображение в различных состояниях приложения. К сожалению, возможностей библиотек тестирования не всегда достаточно для автоматизации проверки визуальных элементов и, в лучшем случае, тестировщик получает возможность проверить размеры элемента, наличие перекрытий с другими элементами и внутренние свойства View, но это не всегда помогает дать однозначный ответ - не была ли сломана верстка в последнем обновлении? Здесь на помощь приходят инструменты для тестирования сравнением с образцом и в этой статье мы рассмотрим подходы к тестированию View и Composable (для Jetpack Compose) с использованием собственных механизмов библиотек и сторонних решений для определения разности между фактическим и эталонным снимков.

Прежде всего начнем с рассмотрения способов захвата визуального представления View или ViewGroup для Android. Самым простым способом создания изображения для View может быть его отрисовка методом draw на Canvas, созданный в памяти. Для определения размера необходимо последовательно measure и layout. Например, можно использовать следующую реализацию:

fun saveGolden(context: Context, bitmap: Bitmap, name: String) {
    val file = File(context.filesDir.absolutePath+File.separator+name)
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, file.outputStream())
}

fun compareWithGolden(context: Context, bitmap: Bitmap, goldenName: String):Boolean {
    val file = File(context.filesDir.absolutePath+File.separator+goldenName)
    val goldenBitmap = BitmapFactory.decodeStream(file.inputStream())
    return bitmap.sameAs(goldenBitmap)
}

fun captureView(view: View, width: Int? = null, height: Int? = null): Bitmap {
    view.measure(
        makeMeasureSpec(width ?: 0, if (width != null) EXACTLY else UNSPECIFIED),
        makeMeasureSpec(height ?: 0, if (height != null) EXACTLY else UNSPECIFIED)
    )
    view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    val bitmap =
        Bitmap.createBitmap(view.measuredWidth, view.measuredHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    view.background?.draw(canvas)
    view.draw(canvas)
    return bitmap
}

Функция captureView возвращает Bitmap-объект для переданного View (или ViewGroup), при этом можно переопределить ширину/высоту или использовать измеренные размеры View. Функция saveGolden сохраняет Bitmap в png-файл (важно не использовать сжатие, чтобы артефакты компрессии не привели к ошибкам при сравнении). Функция compareWithGolden сравнивает полученный bitmap с эталонным и возвращает true при полном совпадении результатов.

Таким образом мы можем делать сравнение визуального представления и эталона для любых View, включая сложные иерархии с вложенными ViewGroup. Метод compareWithGolden можно также использовать при запуске тестов через assert-проверку результата на true. Основная особенность метода в том, что здесь не используется отображение на экране и, как следствие, использовать метод в инструментальном тестировании может быть затруднительно, поскольку в этом случае ожидается проверка текущего состояния на экране. Как можно захватить текущее состояние экрана?

При выполнении инструментального теста можно использовать методы пакета androidx.test.runner.screenshot. Класс ScreenShot содержит статические методы capture() для захвата текущего состояния экрана и capture(activity:Activity) для сохранения состояния указанной Activity. Также есть вариант метода для захвата View: capture(view: View). Результатом выполнения будет объект класса ScreenCapture, из которого можно получить финальное изображение через обращение к свойству bitmap. Дальнейшие действия аналогично ранее рассмотренным сценариям сохранения и сравнения с эталонным изображением.

При использовании оберток вокруг Espresso процесс подготовки снимка экрана может быть даже проще, например в Kaspresso можно вызвать функцию captureScreenshot, которая создаст файл с указанным названием, содержащий текущее отображаемое состояние экрана.

Но все же с захватом полного экрана есть несколько важных ограничений. Прежде всего в сохраняемое изображение будут добавлены также элементы управления оболочки Android (включая строку оповещений) и это не позволит выполнять сравнение с эталонным изображением. Можно решить проблему либо через захват Activity, либо сохранением фрагмента изображения с определением расположения View на экране.

Объект View содержит информацию о координатах и размере (метод getLocationOnScreen заполняет массив целых чисел - координаты x и y, свойства width и height содержат размеры элемента). Дальше можно создать новый Bitmap на основе существующего. Например, для этого можно использовать подобную функцию:

fun getViewOnScreen(screen: Bitmap, view: View): Bitmap {
    var coords = intArrayOf(0, 0)
    view.getLocationOnScreen(coords)
    return Bitmap.createBitmap(screen, coords[0], coords[1], view.width, view.height)
}

Важно отметить, что методы захвата могут не сработать в Android O и более новых, в этом случае вместо них нужно использовать PixelCopy:

fun captureView(view: View, activity: Activity, callback: (Bitmap) -> Unit) {
    activity.window?.let { window ->
        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val locationOfViewInWindow = IntArray(2)
        view.getLocationInWindow(locationOfViewInWindow)
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                PixelCopy.request(
                    window,
                    Rect(
                        locationOfViewInWindow[0],
                        locationOfViewInWindow[1],
                        locationOfViewInWindow[0] + view.width,
                        locationOfViewInWindow[1] + view.height
                    ), bitmap, { copyResult ->
                        if (copyResult == PixelCopy.SUCCESS) {
                            callback(bitmap) }
                        }
                    },
                    Handler(Looper.getMainLooper())
                )
            }
        } catch (e: IllegalArgumentException) {
//process error
            e.printStackTrace()
        }
    }
}

Также в ряде случаев не работает и этот метод и необходимо использовать методы MediaProjection для создания виртуального экрана и захвата экрана.

Кроме Android View также нужно рассмотреть способ захвата изображений из @Composable-функций (для Jetpack Compose). Здесь ситуация немножко проще, поскольку элементы дерева Composable содержат метод captureToImage(), из которого можно получить Bitmap .asAndroidBitmap(). Также для захвата экрана с Compose может использоваться библиотека Compose Screenshot.

Для сравнения изображения с эталоном и определения разности можно использовать библиотеку Shot. Также она поддерживает захват экрана для Android Activity и Jetpack Compose. При необходимости можно создать собственную функцию для пиксельного сравнения Bitmap и визуализации различающихся пикселей при необходимости сложной визуализации различий.

На этом все. В заключение хочу пригласить вас на бесплатный урок, где будут рассмотрены рефлексия в Kotlin (kotlin.reflect) и возможные применения ее для инженера по автоматизации.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 6: ↑6 и ↓0+6
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS