Тесты Compose по умолчанию синхронизируются с вашим пользовательским интерфейсом. Когда вы вызываете утверждение или действие с помощью ComposeTestRule , тест синхронизируется заранее, ожидая, пока дерево пользовательского интерфейса не станет неактивным.
Обычно никаких действий предпринимать не нужно. Однако существуют некоторые исключения, о которых следует знать.
При синхронизации теста ваше приложение Compose отстает во времени благодаря виртуальным часам. Это означает, что тесты Compose не выполняются в реальном времени, поэтому они могут проходить максимально быстро.
Однако, если вы не используете методы синхронизации тестов, перекомпозиция не произойдет, и пользовательский интерфейс будет выглядеть приостановленным.
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions.
var lastSeenValue = 0 // Used to track recompositions.
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition.
// Fails because nothing triggered a recomposition.
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition.
composeTestRule.onNodeWithText("1").assertExists()
}Обратите внимание, что это требование относится только к иерархиям Compose, а не к остальной части приложения.
Отключить автоматическую синхронизацию
Когда вы вызываете утверждение или действие через ComposeTestRule , например assertExists() , ваш тест синхронизируется с пользовательским интерфейсом Compose. В некоторых случаях вам может потребоваться остановить эту синхронизацию и управлять временем самостоятельно. Например, вы можете управлять временем, чтобы делать точные снимки экрана анимации в тот момент, когда пользовательский интерфейс еще занят. Чтобы отключить автоматическую синхронизацию, установите свойство autoAdvance в mainClock в значение false :
composeTestRule.mainClock.autoAdvance = false
Как правило, затем вы сами продвигаете время вперед. Вы можете продвинуться ровно на один кадр с помощью advanceTimeByFrame() или на определенную продолжительность с помощью advanceTimeBy() :
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
простаивающие ресурсы
Compose позволяет синхронизировать тесты и пользовательский интерфейс таким образом, чтобы каждое действие и проверка выполнялись в режиме ожидания, при необходимости ожидая или ускоряя ход часов. Однако некоторые асинхронные операции, результаты которых влияют на состояние пользовательского интерфейса, могут выполняться в фоновом режиме, пока тест о них не знает.
Создайте и зарегистрируйте эти ресурсы, находящиеся в режиме ожидания, в вашем тесте, чтобы они учитывались при определении того, занято ли тестируемое приложение или находится в режиме ожидания. Вам не нужно предпринимать никаких действий, если только вам не потребуется зарегистрировать дополнительные ресурсы, находящиеся в режиме ожидания, например, если вы запускаете фоновое задание, не синхронизированное с Espresso или Compose.
Этот API очень похож на функцию Idling Resources в Espresso и позволяет определить, находится ли тестируемый объект в режиме ожидания или занят. Используйте правило тестирования Compose для регистрации реализации IdlingResource .
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Ручная синхронизация
В некоторых случаях необходимо синхронизировать пользовательский интерфейс Compose с другими частями теста или тестируемого приложения.
Функция waitForIdle() ожидает, пока Compose перейдет в режим ожидания, но ее работа зависит от свойства autoAdvance :
composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.
Обратите внимание, что в обоих случаях waitForIdle() также ожидает завершения незавершенных проходов отрисовки и компоновки .
Кроме того, вы можете переводить часы вперед до тех пор, пока не будет выполнено определенное условие, используя функцию advanceTimeUntil() .
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
Обратите внимание, что данное условие должно проверять состояние, на которое могут влиять эти часы (оно работает только со состоянием Compose).
Оптимизировать тесты анимации
При тестировании высококачественной анимации часто необходимо отключать автоматическое переключение кадров и вручную пошагово просматривать кадры, чтобы проверять промежуточные состояния пользовательского интерфейса. Для таких покадровых циклов используйте метод runWithoutImplicitWait для выполнения проверок. Стандартные запросы к узлам (например onNodeWithTag или fetchSemanticsNode ) запускают неявную синхронизацию, которая избыточна при ручном управлении временем, поэтому их обход значительно ускоряет выполнение тестов.
Правила использования
- Ручное управление временем : используйте этот API, когда
mainClock.autoAdvanceустановлено вfalse, и пользовательский интерфейс находится в известном, стабильном состоянии для текущего кадра. - Выполнение в потоке пользовательского интерфейса : Для обеспечения стабильности дерева пользовательского интерфейса вызовите
runWithoutImplicitWaitв потоке пользовательского интерфейса, например, с помощьюrunOnUiThread. Запуск вне потока пользовательского интерфейса подвергает ваш тест условиям гонки и чтению устаревшего состояния. - Утверждения только для чтения : Блок должен содержать исключительно утверждения только для чтения. Любые действия, изменяющие состояние, должны выполняться вне этого блока.
Пример
@Test fun runWithoutImplicitWaitSample() = runComposeUiTest { setContent { MainScreen() } mainClock.autoAdvance = false // Trigger an animation onNodeWithText("Start Animation").performClick() // Step through the animation frame-by-frame while (hasPendingWork()) { mainClock.advanceTimeByFrame() waitForIdle() runOnUiThread { // Suppress implicit synchronization inside this block to avoid redundant // waits on each node query, making the frame assertions execute much faster. runWithoutImplicitWait { val box1 = onNodeWithTag("Box1").fetchSemanticsNode() val box2 = onNodeWithTag("Box2").fetchSemanticsNode() val box3 = onNodeWithTag("Box3").fetchSemanticsNode() // Assert the exact intermediate state of all three properties for this frame assert(box1.boundsInRoot.right <= box2.boundsInRoot.left) assert(box2.boundsInRoot.right <= box3.boundsInRoot.left) } } } }
Синхронизация основного потока
В Compose Testing теперь поддерживается синхронизация с основным потоком, что позволяет безопасно вызывать waitForIdle — и, как следствие, действия и утверждения Compose UI — непосредственно из основного потока.
Ранее тестирование в Compose строго придерживалось двухпоточной модели: выполнение тестов происходило в фоновом тестовом потоке, а обновления пользовательского интерфейса — в основном потоке. Вызов методов синхронизации, таких как waitForIdle или runOnIdle из основного потока (например, внутри блока runOnUiThread ) приводил к возникновению исключения IllegalStateException поскольку фреймворк применял строгие проверки потоков, чтобы предотвратить синхронизацию с основным потоком.
Благодаря включенной синхронизации основного потока, тестовая среда Compose теперь может продвигать таймер и обрабатывать незавершенную работу даже при выполнении блокирующих вызовов в основном потоке.
Когда использовать синхронизацию основного потока?
Хотя стандартным подходом для чистых тестов Compose остается проведение тестов в фоновом потоке, синхронизация с основным потоком весьма выгодна в нескольких конкретных сценариях:
- Взаимодействие сложных представлений : при тестировании гибридных пользовательских интерфейсов, содержащих как Compose, так и устаревшие представления Android, манипулирование представлениями часто требует выполнения в основном потоке. Теперь вы можете взаимодействовать с представлениями и последовательно проверять узлы Compose без постоянного переключения контекстов потоков.
- Синхронные изменения состояния : если ваша архитектура основана на хранителях состояния, строго привязанных к основному потоку, теперь вы можете изменять состояние и немедленно ждать, пока пользовательский интерфейс Compose стабилизируется, не покидая основной поток.
- Пользовательские средства запуска тестов : Если вы создаете собственную инфраструктуру тестирования или используете среды, в которых средство запуска тестов по своей сути выполняется в основном потоке, тесты Compose теперь выполняются без необходимости делегирования фонового потока.
Пример
Исторически сложилось так, что из-за строгого запрета синхронизации в основном потоке разработчикам приходилось переключаться между фоновым потоком запуска тестов и потоком пользовательского интерфейса, что приводило к разрозненности тестов:
@Test fun testBidirectionalInteropUIUpdates_old() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() } // Jump to Test Thread to verify state settles inside compose composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() // Jump back to Main Thread to verify target view state settles scenario.onFragment { fragment -> assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
При включенной синхронизации основного потока утверждения для иерархий Compose и View могут выполняться в одном блоке:
@Test fun testBidirectionalInteropUIUpdates_new() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
Дождитесь подходящих условий.
Любое условие, зависящее от внешних процессов, таких как загрузка данных или измерения/рисования в Android (то есть, измерения или рисования вне Compose), должно использовать более общую концепцию, например, waitUntil() :
composeTestRule.waitUntil(timeoutMs) { condition }
Вы также можете использовать любой из вспомогательных методов waitUntil :
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
Дополнительные ресурсы
- Тестирование приложений на Android : Главная страница, посвященная тестированию Android, предоставляет более широкий обзор основ и методов тестирования.
- Основы тестирования : Узнайте больше об основных концепциях тестирования Android-приложений.
- Локальные тесты : Вы можете запустить некоторые тесты локально, на своей рабочей станции.
- Инструментальные тесты : Рекомендуется также запускать инструментальные тесты, то есть тесты, которые выполняются непосредственно на устройстве.
- Непрерывная интеграция : Непрерывная интеграция позволяет интегрировать ваши тесты в конвейер развертывания.
- Протестируйте разные размеры экрана : учитывая большое количество доступных пользователям устройств, следует протестировать их работу на экранах разных размеров.
- Espresso : Хотя этот язык программирования предназначен для пользовательских интерфейсов на основе представлений, знание Espresso может быть полезно и для некоторых аспектов тестирования с помощью Compose.