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 bezczynne.
Zwykle nie musisz nic robić. Istnieją jednak pewne przypadki skrajne, o których warto wiedzieć.
Gdy test jest synchronizowany, aplikacja Compose jest przesuwana w czasie za pomocą zegara wirtualnego. Oznacza to, że testy Compose nie są przeprowadzane w czasie rzeczywistym, dzięki czemu mogą przebiegać tak szybko, jak to możliwe.
Jeśli jednak nie użyjesz metod, które synchronizują testy, nie nastąpi rekompozycja, a interfejs 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łujesz asercję lub działanie za pomocą ComposeTestRule, np. assertExists(), test jest synchronizowany z interfejsem Compose. W niektórych przypadkach możesz chcieć zatrzymać tę synchronizację i samodzielnie sterować zegarem. Możesz na przykład kontrolować czas, aby robić dokładne zrzuty ekranu animacji w momencie, gdy interfejs jest nadal zajęty. Aby wyłączyć automatyczną synchronizację, ustaw właściwość autoAdvance w mainClock na false:
composeTestRule.mainClock.autoAdvance = false
Zwykle trzeba wtedy samodzielnie przesunąć czas. Możesz przejść o dokładnie 1 klatkę, naciskając advanceTimeByFrame(), lub o określony czas, naciskając advanceTimeBy():
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Nieaktywne zasoby
Compose może synchronizować testy i interfejs, dzięki czemu każde działanie i asercja są wykonywane w stanie bezczynności, oczekiwania lub przyspieszania zegara, w zależności od potrzeb. Jednak niektóre operacje asynchroniczne, których wyniki wpływają na stan interfejsu, mogą być wykonywane w tle, gdy test nie jest ich świadomy.
Utwórz i zarejestruj te bezczynne zasoby w teście, aby były brane pod uwagę przy określaniu, czy testowana aplikacja jest zajęta czy bezczynna. Nie musisz podejmować żadnych działań, chyba że chcesz zarejestrować dodatkowe bezczynne zasoby, 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 Compose, aby zarejestrować implementację interfejsu IdlingResource.
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
Ręczna synchronizacja
W niektórych przypadkach musisz zsynchronizować interfejs Compose z innymi częściami testu lub testowanej aplikacji.
Funkcja waitForIdle() czeka, aż Compose będzie w stanie bezczynności, 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 przebiegi rysowania i układu.
Możesz też przesuwać zegar do przodu, aż zostanie spełniony określony warunek, za pomocą advanceTimeUntil().
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
Pamiętaj, że dany warunek powinien sprawdzać stan, na który może wpływać ten zegar (działa tylko ze stanem Compose).
Testy animacji Optimize
Podczas testowania animacji o wysokiej jakości często trzeba wyłączyć automatyczne przechodzenie i ręcznie przechodzić przez klatki, aby sprawdzić stany pośrednie interfejsu. W przypadku tych konkretnych pętli klatka po klatce użyj metody runWithoutImplicitWait, aby wykonać asercje. Standardowe zapytania dotyczące węzłów (takie jak onNodeWithTag lub fetchSemanticsNode) wywołują niejawne synchronizacje, które są zbędne, gdy ręcznie sterujesz zegarem, więc 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 parametr
mainClock.autoAdvancejest ustawiony nafalse, a interfejs jest w znanym, stabilnym stanie dla bieżącej klatki. - Wykonywanie wątku UI: aby zapewnić stabilność drzewa UI, wywołaj
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. - Asercje tylko do odczytu: blok powinien zawierać wyłącznie asercje tylko do odczytu. Wszelkie 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 Compose obsługuje teraz synchronizację wątku głównego, co umożliwia bezpieczne wywoływanie waitForIdle – a tym samym działań i asercji interfejsu Compose – bezpośrednio z wątku głównego.
Wcześniej testowanie Compose ściśle egzekwowało model dwuwątkowy: wykonanie testu odbywało się w wątku testowym w tle, a aktualizacje interfejsu odbywały się w głównym wątku. Wywołanie metod synchronizacji, takich jak waitForIdle lub runOnIdle, z wątku głównego (np. wewnątrz bloku runOnUiThread) spowodowałoby zgłoszenie błędu IllegalStateException, ponieważ platforma wymusza rygorystyczne 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 gdy w wątku głównym są wykonywane wywołania blokujące.
Kiedy używać synchronizacji wątku głównego
Chociaż standardem w przypadku testów Compose jest przeprowadzanie ich w wątku w tle, synchronizacja wątku głównego jest bardzo korzystna w kilku konkretnych scenariuszach:
- Złożona interoperacyjność widoków: podczas testowania hybrydowych interfejsów użytkownika zawierających zarówno Compose, jak i starsze widoki Androida, manipulowanie widokami często wymaga uruchamiania w głównym wątku. Możesz teraz wchodzić w interakcje z widokami i wykonywać asercje na węzłach Compose sekwencyjnie, bez ciągłego przełączania kontekstów wątków.
- Synchroniczne mutacje stanu: jeśli Twoja architektura opiera się na zmiennych stanu ściśle powiązanych z głównym wątkiem, możesz teraz mutować stan i natychmiast czekać na ustabilizowanie się interfejsu Compose bez opuszczania głównego wątku.
- Niestandardowe moduły uruchamiające testy: jeśli tworzysz niestandardową infrastrukturę testową lub korzystasz ze środowisk, w których moduł uruchamiający testy jest z natury 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, ponieważ synchronizacja była surowo zabroniona w głównym wątku, deweloperzy musieli przełączać się między wątkiem uruchamiającym testy 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") } }
Po włączeniu synchronizacji wątku głównego 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") } }
Czekaj na warunki
W przypadku każdego warunku, który zależy od zewnętrznych zadań, takich jak wczytywanie danych lub pomiar albo rysowanie w Androidzie (czyli pomiar lub rysowanie poza Compose), należy użyć bardziej ogólnej koncepcji, takiej jak waitUntil():
composeTestRule.waitUntil(timeoutMs) { condition }
Możesz też użyć dowolnego z tych pomocników waitUntil:
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 na Androidzie zawiera szerszy przegląd podstaw i technik testowania.
- Podstawy testowania: dowiedz się więcej o podstawowych pojęciach związanych z testowaniem aplikacji na Androida.
- Testy lokalne: niektóre testy możesz przeprowadzić lokalnie, na własnej stacji roboczej.
- Testy z instrumentacją: warto też przeprowadzać testy z instrumentacją. czyli testy, które są uruchamiane bezpośrednio na urządzeniu.
- Tryb ciągłej integracji: tryb ciągłej integracji umożliwia integrację testów z potokiem wdrażania.
- Testowanie różnych rozmiarów ekranu: użytkownicy mają do dyspozycji wiele urządzeń, dlatego warto przetestować różne rozmiary ekranu.
- Espresso: chociaż Espresso jest przeznaczone do interfejsów opartych na widokach, wiedza o nim może być przydatna w niektórych aspektach testowania Compose.