Testy Compose są domyślnie synchronizowane z interfejsem. Gdy wywołujesz
potwierdzenie lub działanie za pomocą ComposeTestRule, test jest wcześniej synchronizowany
i czeka, aż drzewo interfejsu będzie nieaktywne.
Zwykle nie musisz podejmować żadnych działań. Musisz jednak znać kilka przypadków brzegowych.
Gdy test jest synchronizowany, czas w aplikacji Compose jest przyspieszany za pomocą wirtualnego zegara. Oznacza to, że testy Compose nie są przeprowadzane w czasie rzeczywistym, dzięki czemu 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 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 ten wymóg dotyczy tylko hierarchii Compose, a nie reszty aplikacji.
Wyłączanie automatycznej synchronizacji
Gdy wywołujesz potwierdzenie 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 sterować czasem, 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 będziesz wtedy samodzielnie przyspieszać czas. Możesz przyspieszyć czas o dokładnie 1 klatkę za pomocą advanceTimeByFrame() lub o określony czas trwania za pomocą advanceTimeBy():
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
Nieaktywne zasoby
Compose może synchronizować testy i interfejs, tak aby każde działanie i potwierdzenie było wykonywane w stanie nieaktywnym, w razie potrzeby czekając lub przyspieszając zegar. Niektóre operacje asynchroniczne, których wyniki wpływają na stan interfejsu, mogą jednak być wykonywane w tle, a test nie będzie o nich wiedział.
Utwórz i zarejestruj te nieaktywne zasoby w teście, aby były uwzględniane podczas określania, czy testowana aplikacja jest zajęta, czy nieaktywna. Nie musisz podejmować żadnych działań, chyba że chcesz zarejestrować dodatkowe nieaktywne zasoby, np. jeśli uruchamiasz zadanie w tle, które nie jest zsynchronizowane z Espresso ani Compose.
Ten interfejs API jest bardzo podobny do nieaktywnych zasobów Espresso, które wskazują, czy
testowany obiekt jest nieaktywny, czy zajęty. Aby zarejestrować
implementację IdlingResource, użyj reguły testu Compose.
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ż Compose będzie nieaktywny, ale funkcja
zależy od właściwości autoAdvance property:
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 rysowanie i układ
pasy.
Możesz też przyspieszyć zegar, aż zostanie spełniony określony warunek, za pomocą
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 tylko ze stanem Compose).
Optymalizowanie testów animacji
When testing high-fidelity animations, you often need to disable auto-advance
and manually step through frames to assert intermediate UI states. For these
specific frame-by-frame loops, use the runWithoutImplicitWait method to
execute your assertions. Standard node queries (like onNodeWithTag or
fetchSemanticsNode) trigger implicit synchronizations that are redundant
when you are manually controlling the clock, so bypassing them significantly
speeds up your test runtimes.
Usage guidelines
- Manual clock management: Use this API when
mainClock.autoAdvanceis set tofalseand the UI is in a known, stable state for the current frame. - UI thread execution: To ensure the stability of the UI tree, call
runWithoutImplicitWaiton the UI thread, such as withrunOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads. - Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.
Example
@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 pozwala bezpiecznie wywoływać waitForIdle – a co za tym idzie, działania i potwierdzenia interfejsu Compose – bezpośrednio z wątku głównego.
Wcześniej testowanie Compose ściśle wymuszało model 2 wątków: wykonywanie testu odbywało się w wątku testu w tle, a aktualizacje interfejsu – w wątku głównym. Wywoływanie metod synchronizacji, takich jak waitForIdle czy runOnIdle, z wątku głównego (np. w bloku runOnUiThread) powodowało zgłoszenie IllegalStateException, ponieważ framework wymuszał ścisłe sprawdzanie wątków, aby zapobiec synchronizacji wątku głównego.
Gdy synchronizacja wątku głównego jest włączona, framework testowania Compose może teraz przyspieszać zegar i przetwarzać oczekujące zadania nawet wtedy, gdy w wątku głównym są wykonywane blokujące wywołania.
Kiedy używać synchronizacji wątku głównego
Chociaż standardem w przypadku testów Compose jest utrzymywanie testów 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 zawierających zarówno widoki Compose, jak i starsze widoki Androida, manipulowanie widokami często wymaga uruchamiania w wątku głównym. Możesz teraz kolejno wchodzić w interakcje z widokami i potwierdzać węzły Compose bez ciągłego przełączania kontekstów wątków.
- Synchroniczne mutacje stanu: jeśli Twoja architektura opiera się na posiadaczach stanu ściśle powiązanych z wątkiem głównym, możesz teraz zmieniać stan i natychmiast czekać aż interfejs Compose się ustabilizuje, bez opuszczania wątku głównego.
- 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 z natury wykonywany w wątku głównym, testy Compose są teraz wykonywane bez konieczności delegowania do wątku w tle.
Przykład
W przeszłości, ponieważ synchronizacja w wątku głównym była ściśle zabroniona, deweloperzy musieli przełączać się między wątkiem programu do uruchamiania testów 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, potwierdzenia hierarchii Compose i widoków 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") } }
Czekanie na warunki
Każdy warunek, który zależy od pracy zewnętrznej, np. wczytywania danych lub pomiaru czy rysowania w Androidzie (czyli pomiaru lub rysowania poza Compose), powinien używać
bardziej ogólnego pojęcia, takiego jak waitUntil():
composeTestRule.waitUntil(timeoutMs) { condition }
Możesz też użyć dowolnego z
waitUntil pomocników:
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 Androida zawiera szersze omówienie podstaw i technik testowania.
- Podstawy testowania: dowiedz się więcej o podstawowych koncepcjach testowania aplikacji na Androida.
- Testy lokalne: niektóre testy możesz przeprowadzać lokalnie, na własnej stacji roboczej.
- Testy instrumentowane: warto też przeprowadzać testy instrumentowane. 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 na różnych rozmiarach ekranu: użytkownicy mają do dyspozycji wiele urządzeń, dlatego warto testować na różnych rozmiarach ekranu.
- Espresso: chociaż Espresso jest przeznaczone do interfejsów opartych na widokach, wiedza na jego temat może być przydatna w niektórych aspektach testowania Compose.