Testy Compose są domyślnie synchronizowane z interfejsem. Gdy wywołujesz asercję lub działanie za pomocą ComposeTestRule, test jest wcześniej synchronizowany i czeka, aż drzewo interfejsu będzie nieaktywne.
Zwykle nie musisz nic robić. Istnieją jednak pewne przypadki skrajne, o których warto wiedzieć.
Gdy test jest zsynchronizowany, aplikacja napisana w Compose jest przesuwana w czasie za pomocą zegara wirtualnego. Oznacza to, że testy Compose nie są przeprowadzane w czasie rzeczywistym, więc mogą zakończyć się tak szybko, jak to możliwe.
Jeśli jednak nie użyjesz metod synchronizujących testy, nie nastąpi rekompozycja i interfejs użytkownika będzie wyglądał na wstrzymany.
@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()
}Pamiętaj, że to wymaganie dotyczy tylko hierarchii Compose, a nie reszty aplikacji.
Wyłączanie automatycznej synchronizacji
Gdy wywołasz asercję lub działanie za pomocą ComposeTestRule, np.assertExists(), test zostanie zsynchronizowany z interfejsem Compose. W niektórych przypadkach możesz chcieć zatrzymać tę synchronizację i samodzielnie kontrolować zegar. Możesz na przykład kontrolować czas, aby robić dokładne zrzuty ekranu animacji w momencie, gdy interfejs użytkownika jest nadal zajęty. Aby wyłączyć automatyczną synchronizację, ustaw właściwość autoAdvance w mainClock na false:
composeTestRule.mainClock.autoAdvance = false
Zwykle czas jest wtedy przesuwany ręcznie. Możesz przejść dokładnie o 1 klatkę za pomocą przycisku advanceTimeByFrame() lub o określony czas za pomocą przycisku advanceTimeBy():
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Nieaktywne zasoby
Compose może synchronizować testy i interfejs, aby każde działanie i każde sprawdzenie było wykonywane w stanie bezczynności, w razie potrzeby oczekując lub przesuwając zegar. Niektóre operacje asynchroniczne, których wyniki wpływają na stan interfejsu, mogą być jednak wykonywane w tle, a test nie będzie o nich wiedzieć.
Utwórz i zarejestruj w teście te zasoby bezczynne, aby były uwzględniane podczas określania, czy testowana aplikacja jest zajęta czy bezczynna. Nie musisz podejmować żadnych działań, chyba że chcesz zarejestrować dodatkowe zasoby bezczynne, np. jeśli uruchamiasz zadanie w tle, które nie jest zsynchronizowane z Espresso ani Compose.
Ten interfejs API jest bardzo podobny do zasobów bezczynnych Espresso, które wskazują, czy testowany obiekt jest bezczynny, czy zajęty. Użyj reguły testowej tworzenia, aby zarejestrować wdrożenie IdlingResource.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Synchronizacja ręczna
W niektórych przypadkach musisz zsynchronizować interfejs Compose z innymi częściami testu lub testowanej aplikacji.
Funkcja waitForIdle() czeka, aż funkcja Compose będzie bezczynna, ale zależy od właściwości 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.
Pamiętaj, że w obu przypadkach waitForIdle() czeka też na oczekujące przejścia rysowania i układu.
Możesz też przyspieszyć zegar, aż zostanie spełniony określony warunek, za pomocą polecenia advanceTimeUntil().
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
Pamiętaj, że podany warunek powinien sprawdzać stan, na który może wpływać ten zegar (działa on tylko ze stanem Compose).
Optymalizowanie testów animacji
测试高保真动画时,您通常需要停用自动前进功能,并手动逐帧浏览,以断言中间界面状态。对于这些特定的逐帧循环,请使用 runWithoutImplicitWait 方法来执行断言。当您手动控制时钟时,标准节点查询(例如 onNodeWithTag 或 fetchSemanticsNode)会触发冗余的隐式同步,因此绕过这些查询可以显著缩短测试运行时长。
使用指南
- 手动时钟管理:当
mainClock.autoAdvance设置为false且界面处于当前帧的已知稳定状态时,请使用此 API。 - 界面线程执行:为确保界面树的稳定性,请在界面线程上调用
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) } } } }
Synchronizacja wątku głównego
Testowanie w Compose obsługuje teraz synchronizację wątku głównego, co umożliwia bezpieczne wywoływanie funkcji waitForIdle, a tym samym działań i asercji interfejsu Compose, bezpośrednio z wątku głównego.
Wcześniej testowanie Compose ściśle wymuszało model dwuwątkowy: wykonywanie testu odbywało się w wątku testu w tle, a aktualizacje interfejsu użytkownika – w wątku głównym. Wywoływanie metod synchronizacji, takich jak waitForIdle lub runOnIdle, z wątku głównego (np. w bloku runOnUiThread) powoduje zgłoszenie wyjątku IllegalStateException, ponieważ platforma wymusza ścisłe sprawdzanie wątków, aby zapobiec synchronizacji wątku głównego.
Po włączeniu synchronizacji wątku głównego platforma testowa Compose może teraz przesuwać zegar i przetwarzać oczekujące zadania nawet wtedy, gdy w wątku głównym wykonywane są wywołania blokujące.
Kiedy używać synchronizacji wątku głównego
Chociaż utrzymywanie testów w wątku w tle pozostaje standardem w przypadku testów czystego Compose, synchronizacja wątku głównego jest bardzo korzystna w kilku konkretnych scenariuszach:
- Interoperacyjność złożonych widoków: podczas testowania hybrydowych interfejsów, które zawierają zarówno widoki Compose, jak i starsze widoki Androida, manipulowanie widokami często wymaga uruchamiania na głównym wątku. Możesz teraz wchodzić w interakcje z widokami i sprawdzać węzły Compose sekwencyjnie bez ciągłego przełączania kontekstów wątków.
- Synchroniczne zmiany stanu: jeśli Twoja architektura opiera się na ściśle powiązanych z głównym wątkiem elementach przechowujących stan, możesz teraz zmieniać stan i natychmiast czekać na ustabilizowanie się interfejsu Compose bez opuszczania głównego wątku.
- Niestandardowe programy do uruchamiania testów: jeśli tworzysz niestandardową infrastrukturę testową lub korzystasz ze środowisk, w których program do uruchamiania testów jest wykonywany w głównym wątku, testy Compose są teraz wykonywane bez konieczności delegowania do wątku w tle.
Przykład
W przeszłości synchronizacja w głównym wątku była surowo zabroniona, więc programiści musieli przełączać się między wątkiem narzędzia do testowania w tle a wątkiem UI, co prowadziło do niespójnych testów:
@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") } }
Gdy synchronizacja wątku głównego jest włączona, asercje dotyczące hierarchii Compose i View można wykonywać w tym samym bloku:
@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") } }
Oczekiwanie na warunki
Każdy warunek, który zależy od pracy zewnętrznej, np. wczytywania danych lub pomiaru lub rysowania w Androidzie (czyli pomiaru lub rysowania poza kompozycją), powinien używać bardziej ogólnej koncepcji, takiej jak waitUntil():
composeTestRule.waitUntil(timeoutMs) { condition }
Możesz też użyć dowolnego z tych waitUntilnarzędzi:
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
Dodatkowe materiały
- Testowanie aplikacji na Androida: główna strona docelowa testowania aplikacji na Androida zawiera szersze omówienie podstaw testowania i technik testowania.
- Podstawy testowania: dowiedz się więcej o podstawowych koncepcjach związanych z testowaniem aplikacji na Androida.
- Testy lokalne: niektóre testy możesz przeprowadzać lokalnie, na własnej stacji roboczej.
- Testy z użyciem instrumentacji: warto też przeprowadzać testy z użyciem instrumentacji. Są to testy, które są przeprowadzane bezpośrednio na urządzeniu.
- Tryb ciągłej integracji: Tryb ciągłej integracji umożliwia zintegrowanie testów z potokiem wdrażania.
- Testowanie różnych rozmiarów ekranu: użytkownicy mają do dyspozycji wiele urządzeń, dlatego warto przeprowadzać testy na różnych rozmiarach ekranu.
- Espresso: chociaż Espresso jest przeznaczony do interfejsów opartych na widokach, wiedza o nim może być przydatna w przypadku niektórych aspektów testowania Compose.