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
Podczas testowania animacji o wysokiej jakości często trzeba wyłączyć automatyczne przechodzenie do następnej klatki i ręcznie przechodzić przez klatki, aby potwierdzić pośrednie stany interfejsu. W przypadku tych
konkretnych pętli klatka po klatce użyj metody runWithoutImplicitWait, aby
wykonać potwierdzenia. Standardowe zapytania o węzły (np. onNodeWithTag lub
fetchSemanticsNode) wywołują niejawne synchronizacje, które są zbędne
gdy ręcznie sterujesz zegarem. Ich pominięcie znacznie
przyspiesza czas wykonywania testów.
Wytyczne dotyczące użytkowania
- Ręczne zarządzanie zegarem: używaj tego interfejsu API, gdy
mainClock.autoAdvancejest ustawione nafalse, a interfejs jest w znanym, stabilnym stanie dla bieżącej klatki. - Wykonywanie w wątku UI: aby zapewnić stabilność drzewa interfejsu, wywołuj
runWithoutImplicitWaitw wątku UI, np. za pomocąrunOnUiThread. Uruchomienie go poza wątkiem UI naraża test na sytuacje wyścigu i odczytywanie nieaktualnego stanu. - Potwierdzenia tylko do odczytu: blok powinien zawierać wyłącznie potwierdzenia tylko do odczytu. Wszystkie działania, które zmieniają stan, powinny być wykonywane poza tym blokiem.
Przykład
@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.