Большая стабильность тестов

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

Современные платформы, такие как Compose или Espresso, разработаны с учетом тестирования, поэтому существует определенная гарантия того, что пользовательский интерфейс будет простаивать перед следующим тестовым действием или утверждением. Это синхронизация .

Тестовая синхронизация

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

блок-схема, показывающая цикл, который проверяет, простаивает ли приложение, перед выполнением теста
Рис. 1. Тестовая синхронизация.

Чтобы повысить надежность вашего набора тестов, вы можете установить способ отслеживания фоновых операций, например Espresso Idling Resources . Кроме того, вы можете заменить модули для тестовых версий, которые можно запросить на предмет простоя или которые улучшают синхронизацию, например TestDispatcher для сопрограмм или RxIdler для RxJava.

Диаграмма, показывающая сбой теста, когда синхронизация основана на ожидании фиксированного времени
Рисунок 2. Использование сна в тестах приводит к медленным или нестабильным тестам.

Способы повышения стабильности

Большие тесты могут выявить множество регрессий одновременно, поскольку они тестируют несколько компонентов приложения. Обычно они работают на эмуляторах или устройствах, что означает высокую точность воспроизведения. Хотя крупные сквозные тесты обеспечивают всесторонний охват, они более склонны к случайным сбоям.

Основные меры, которые можно предпринять для уменьшения шелушения, следующие:

  • Настройте устройства правильно
  • Предотвращение проблем с синхронизацией
  • Реализация повторных попыток

Чтобы создать большие тесты с помощью Compose или Espresso , вы обычно запускаете одно из своих действий и перемещаетесь, как это делает пользователь, проверяя правильность поведения пользовательского интерфейса с помощью утверждений или тестов скриншотов.

Другие платформы, такие как UI Automator , предоставляют более широкие возможности, поскольку вы можете взаимодействовать с системным пользовательским интерфейсом и другими приложениями. Однако тесты UI Automator могут потребовать большей ручной синхронизации, поэтому они, как правило, менее надежны.

Настройка устройств

Во-первых, чтобы повысить надежность ваших тестов, вам следует убедиться, что операционная система устройства не прерывает неожиданно выполнение тестов. Например, когда диалоговое окно обновления системы отображается поверх других приложений или когда на диске недостаточно места.

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

Устройства, управляемые Gradle

Если вы сами управляете эмуляторами, вы можете использовать устройства, управляемые Gradle, чтобы определить, какие устройства использовать для запуска ваших тестов:

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

В этой конфигурации следующая команда создаст образ эмулятора, запустит экземпляр, запустит тесты и завершит его работу.

./gradlew pixel2api30DebugAndroidTest

Устройства, управляемые Gradle, содержат механизмы повторной попытки в случае отключения устройства и других улучшений.

Предотвращение проблем с синхронизацией

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

Решения

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

Вместо оценки того, занято ли действие или нет, вы можете заставить тесты ждать, пока не будут выполнены определенные условия. Например, вы можете подождать, пока в пользовательском интерфейсе не отобразится определенный текст или компонент.

Compose имеет набор API-интерфейсов тестирования как часть ComposeTestRule для ожидания различных сопоставлений:

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

И общий API, который принимает любую функцию, возвращающую логическое значение:

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

Пример использования:

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

Механизмы повторных попыток

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

Повторные попытки должны происходить на нескольких уровнях, чтобы предотвратить проблемы, такие как:

  • Время подключения к устройству истекло или соединение потеряно
  • Одиночный провал теста

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

  • Правило JUnit, которое повторяет любой тест несколько раз.
  • Повторное действие или шаг в рабочем процессе CI
  • Система перезапуска эмулятора, когда он не отвечает, например устройств, управляемых Gradle.