Menyinkronkan pengujian Anda

Uji Compose disinkronkan secara default dengan UI Anda. Saat Anda memanggil pernyataan atau tindakan dengan ComposeTestRule, pengujian akan disinkronkan terlebih dahulu, menunggu hingga pohon UI tidak aktif.

Biasanya, Anda tidak perlu melakukan tindakan apa pun. Namun, ada beberapa kasus ekstrem yang harus Anda ketahui.

Saat pengujian disinkronkan, aplikasi Compose dimajukan menggunakan jam virtual. Ini berarti pengujian Compose tidak berjalan secara real time, sehingga dapat lulus secepat mungkin.

Namun, jika Anda tidak menggunakan metode yang menyinkronkan pengujian, tidak ada rekomposisi yang akan terjadi dan UI akan dijeda.

@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()
}

Perhatikan bahwa persyaratan ini hanya berlaku untuk hierarki Compose dan tidak untuk aplikasi lainnya.

Menonaktifkan sinkronisasi otomatis

Saat Anda memanggil pernyataan atau tindakan melalui ComposeTestRule seperti assertExists(), pengujian Anda akan disinkronkan dengan Compose UI. Dalam beberapa kasus, Anda mungkin ingin menghentikan sinkronisasi ini dan mengontrol sendiri jamnya. Misalnya, Anda dapat mengontrol waktu untuk mengambil screenshot animasi yang akurat pada suatu titik dan UI akan tetap sibuk. Untuk menonaktifkan sinkronisasi otomatis, setel properti autoAdvance dalam mainClock ke false:

composeTestRule.mainClock.autoAdvance = false

Biasanya Anda kemudian akan memajukan waktu sendiri. Anda dapat memajukan satu frame dengan advanceTimeByFrame() atau berdasarkan durasi tertentu dengan advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Resource nonaktif

Compose dapat menyinkronkan pengujian dan UI sehingga setiap tindakan dan pernyataan dilakukan dalam status tidak ada aktivitas, menunggu, atau mendukung jam sesuai kebutuhan. Namun, beberapa operasi asinkron yang hasilnya memengaruhi status UI dapat dijalankan di latar belakang saat pengujian tidak menyadarinya.

Buat dan daftarkan resource tidak ada aktivitas ini dalam pengujian sehingga resource tersebut diperhitungkan saat memutuskan apakah aplikasi yang sedang diuji sibuk atau tidak. Anda tidak perlu melakukan tindakan apa pun kecuali jika perlu mendaftarkan resource tidak ada aktivitas tambahan, misalnya, jika Anda menjalankan tugas latar belakang yang tidak disinkronkan dengan Espresso atau Compose.

API ini sangat mirip dengan Idling Resources Espresso untuk menunjukkan apakah subjek dalam pengujian sedang tidak ada aktivitas atau sibuk. Gunakan aturan pengujian Compose untuk mendaftarkan penerapan IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Sinkronisasi manual

Dalam kasus tertentu, Anda harus menyinkronkan Compose UI dengan bagian lain dari pengujian atau aplikasi yang sedang Anda uji.

Fungsi waitForIdle() menunggu Compose menjadi tidak ada aktivitas, tetapi fungsi bergantung pada properti 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.

Perhatikan bahwa dalam kedua kasus tersebut, waitForIdle() juga menunggu proses gambar dan tata letak yang tertunda.

Selain itu, Anda dapat meningkatkan waktu hingga kondisi tertentu terpenuhi dengan advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

Perhatikan bahwa ketentuan yang diberikan harus memeriksa status yang dapat dipengaruhi oleh jam ini (hanya berfungsi dengan status Compose).

Mengoptimalkan pengujian animasi

Saat menguji animasi fidelitas tinggi, Anda sering kali perlu menonaktifkan auto-advance dan melangkah secara manual melalui frame untuk menegaskan status UI perantara. Untuk loop frame demi frame tertentu ini, gunakan metode runWithoutImplicitWait untuk menjalankan pernyataan Anda. Kueri node standar (seperti onNodeWithTag atau fetchSemanticsNode) memicu sinkronisasi implisit yang berlebihan saat Anda mengontrol jam secara manual, sehingga melewati sinkronisasi tersebut akan mempercepat runtime pengujian secara signifikan.

Panduan penggunaan

  • Pengelolaan jam manual: Gunakan API ini saat mainClock.autoAdvance ditetapkan ke false dan UI berada dalam status yang diketahui dan stabil untuk frame saat ini.
  • Eksekusi UI thread: Untuk memastikan stabilitas pohon UI, panggil runWithoutImplicitWait di UI thread, seperti dengan runOnUiThread. Menjalankannya di luar UI thread akan mengekspos pengujian Anda ke kondisi race dan pembacaan status yang tidak valid.
  • Pernyataan hanya baca: Blok harus berisi pernyataan hanya baca saja. Tindakan apa pun yang mengubah status harus dilakukan di luar blok ini.

Contoh

@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)
            }
        }
    }
}

Sinkronisasi thread utama

Pengujian Compose kini mendukung sinkronisasi thread utama, sehingga Anda dapat memanggil waitForIdle— dan dengan demikian, tindakan dan pernyataan UI Compose — secara aman langsung dari thread utama.

Sebelumnya, pengujian Compose secara ketat menerapkan model dua thread: eksekusi pengujian terjadi di thread pengujian latar belakang, sementara update UI terjadi di thread utama. Memanggil metode sinkronisasi seperti waitForIdle atau runOnIdle dari thread utama (misalnya, di dalam blok runOnUiThread) akan memunculkan IllegalStateException karena framework menerapkan pemeriksaan thread yang ketat untuk mencegah sinkronisasi thread utama.

Dengan sinkronisasi thread utama diaktifkan, framework pengujian Compose kini dapat memajukan clock dan memproses pekerjaan yang tertunda meskipun panggilan pemblokiran dilakukan di thread utama.

Kapan harus menggunakan sinkronisasi thread utama

Meskipun menjaga pengujian di thread latar belakang tetap menjadi standar untuk pengujian Compose murni, sinkronisasi thread utama sangat menguntungkan dalam beberapa skenario tertentu:

  • Interoperabilitas View yang kompleks: Saat menguji UI hybrid yang berisi Compose dan View Android lama, memanipulasi View sering kali memerlukan eksekusi di thread utama. Anda kini dapat berinteraksi dengan Tampilan dan menegaskan pada node Compose secara berurutan tanpa terus-menerus beralih konteks thread.
  • Mutasi status sinkron: Jika arsitektur Anda bergantung pada pemegang status yang terikat secara ketat ke thread utama, Anda kini dapat memutasi status dan segera menunggu UI Compose diselesaikan tanpa keluar dari thread utama.
  • Peluncur pengujian kustom: Jika Anda sedang membangun infrastruktur pengujian kustom atau menggunakan lingkungan tempat peluncur pengujian secara inheren dieksekusi di thread utama, pengujian Compose kini dieksekusi dengan bersih tanpa memerlukan delegasi thread latar belakang.

Contoh

Sebelumnya, karena sinkronisasi dilarang keras di thread utama, developer harus berpindah-pindah antara thread peluncuran pengujian latar belakang dan thread UI, sehingga menyebabkan pengujian yang tidak terhubung:

@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")
    }
}

Dengan mengaktifkan sinkronisasi thread utama, pernyataan untuk hierarki Compose dan View dapat dieksekusi dalam blok yang sama:

@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")
    }
}

Menunggu kondisi

Setiap kondisi yang bergantung pada pekerjaan eksternal, seperti pemuatan data atau pengukuran atau gambar Android (yaitu, mengukur atau menggambar eksternal untuk Compose), harus menggunakan konsep yang lebih umum seperti waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Anda juga dapat menggunakan salah satu pembantu waitUntil:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Referensi Tambahan

  • Menguji aplikasi di Android: Halaman landing utama pengujian Android memberikan gambaran yang lebih luas tentang dasar-dasar dan teknik pengujian.
  • Dasar-dasar pengujian: Pelajari lebih lanjut konsep inti di balik pengujian aplikasi Android.
  • Pengujian lokal: Anda dapat menjalankan beberapa pengujian secara lokal, di workstation Anda sendiri.
  • Pengujian berinstrumen: Sebaiknya jalankan juga pengujian berinstrumen. Artinya, pengujian yang berjalan langsung di perangkat.
  • Continuous integration: Continuous integration memungkinkan Anda mengintegrasikan pengujian ke dalam pipeline deployment.
  • Uji berbagai ukuran layar: Dengan banyaknya perangkat yang tersedia bagi pengguna, Anda harus menguji berbagai ukuran layar.
  • Espresso: Meskipun ditujukan untuk UI berbasis View, pengetahuan Espresso masih dapat membantu untuk beberapa aspek pengujian Compose.