Pengujian di Jetpack Compose

1. Pengantar dan penyiapan

Dalam codelab ini, Anda akan mempelajari UI pengujian yang dibuat dengan Jetpack Compose. Anda akan menulis pengujian pertama sambil mempelajari pengujian secara terpisah, pengujian proses debug, hierarki semantik, dan sinkronisasi.

Yang Anda butuhkan

Memeriksa kode untuk codelab ini (Rally)

Anda akan menggunakan studi Material Rally sebagai dasar untuk codelab ini. Anda akan menemukannya di repositori GitHub android-compose-codelabs. Untuk meng-clone, jalankan:

git clone https://github.com/android/codelab-android-compose.git

Setelah didownload, buka project TestingCodelab.

Atau, Anda dapat mendownload dua file ZIP:

Buka folder TestingCodelab yang berisi aplikasi bernama Rally.

Memeriksa struktur project

Pengujian Compose adalah pengujian berinstrumen. Artinya, diperlukan perangkat (fisik atau emulator) untuk menjalankan pengujian.

Rally sudah berisi sejumlah pengujian UI Berinstrumen. Anda dapat menemukannya di set sumber androidTest:

b14721ae60ee9022.png

Ini adalah direktori tempat Anda akan menempatkan pengujian baru. Anda dapat melihat file AnimatingCircleTests.kt untuk mengetahui tampilan pengujian Compose.

Rally sudah dikonfigurasi, tetapi yang Anda perlukan untuk mengaktifkan pengujian Compose dalam project baru hanya dependensi pengujian dalam file build.gradle modul yang relevan, yakni:

androidTestImplementation "androidx.compose.ui:ui-test-junit4:$version"

debugImplementation "androidx.compose.ui:ui-test-manifest:$rootProject.composeVersion"

Jangan ragu untuk menjalankan aplikasi dan memahaminya.

2. Yang perlu diuji

Kita akan fokus pada panel tab Rally, yang berisi baris tab (Ringkasan, Akun, dan Tagihan). Tampilannya seperti ini dalam konteks:

19c6a7eb9d732d37.gif

Dalam codelab ini, Anda akan menguji UI panel.

Ini bisa berarti banyak hal:

  • Menguji apakah tab menampilkan ikon dan teks yang diinginkan
  • Menguji apakah animasi cocok dengan spesifikasi
  • Menguji apakah peristiwa navigasi yang dipicu sudah benar
  • Menguji penempatan dan jarak elemen UI dalam berbagai status
  • Mengambil screenshot panel dan membandingkannya dengan screenshot sebelumnya

Tidak ada aturan yang pasti tentang berapa banyak atau cara menguji komponen. Anda dapat melakukan semua hal di atas. Dalam codelab ini, Anda akan menguji apakah logika status sudah benar dengan memverifikasi bahwa:

  • Tab hanya menampilkan labelnya saat dipilih.
  • Layar aktif menentukan tab yang dipilih

3. Membuat pengujian UI sederhana

Membuat file TopAppBarTest

Buat file baru di folder yang sama sebagai AnimatingCircleTests.kt (app/src/androidTest/com/example/compose/rally), lalu beri nama TopAppBarTest.kt.

Compose terdiri atas ComposeTestRule yang dapat Anda peroleh dengan memanggil createComposeRule(). Aturan ini memungkinkan Anda menetapkan konten Compose yang diuji dan berinteraksi dengannya.

Menambahkan ComposeTestRule

package com.example.compose.rally

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    // TODO: Add tests
}

Pengujian secara terpisah

Dalam pengujian Compose, kita dapat memulai aktivitas utama aplikasi dengan cara yang sama seperti yang Anda lakukan di dunia Tampilan Android menggunakan Espresso, misalnya. Anda dapat melakukannya dengan createAndroidComposeRule.

// Don't copy this over

@get:Rule
val composeTestRule = createAndroidComposeRule(RallyActivity::class.java)

Namun, dengan Compose, kita dapat menyederhanakan cukup banyak hal dengan menguji komponen secara terpisah. Anda dapat memilih konten UI Compose yang akan digunakan dalam pengujian. Pengujian ini dilakukan dengan metode setContent ComposeTestRule, dan Anda dapat memanggilnya di mana pun (tetapi hanya sekali).

// Don't copy this over

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun myTest() {
        composeTestRule.setContent {
            Text("You can set any Compose content!")
        }
    }
}

Kita ingin menguji TopAppBar, jadi mari fokus pada hal itu. Panggil RallyTopAppBar di dalam setContent, lalu biarkan Android Studio melengkapi nama parameter.

import androidx.compose.ui.test.junit4.createComposeRule
import com.example.compose.rally.ui.components.RallyTopAppBar
import org.junit.Rule
import org.junit.Test

class TopAppBarTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun rallyTopAppBarTest() {
        composeTestRule.setContent {
            RallyTopAppBar(
                allScreens = ,
                onTabSelected = { /*TODO*/ },
                currentScreen =
            )
        }
    }
}

Pentingnya Composable yang dapat diuji

RallyTopAppBar menggunakan tiga parameter yang mudah diberikan sehingga kita dapat meneruskan data palsu yang dikontrol. Contoh:

    @Test
    fun rallyTopAppBarTest() {
        val allScreens = RallyScreen.values().toList()
        composeTestRule.setContent {
            RallyTopAppBar(
                allScreens = allScreens,
                onTabSelected = { },
                currentScreen = RallyScreen.Accounts
            )
        }
        Thread.sleep(5000)
    }

Kita juga menambahkan sleep() sehingga Anda dapat melihat apa yang sedang terjadi. Klik kanan rallyTopAppBarTest, lalu klik "Run rallyTopAppBarTest()...".

baca545ddc8c3fa9.png

Pengujian menampilkan panel aplikasi teratas (selama 5 detik), tetapi tidak seperti yang kita harapkan: temanya terang.

Alasannya adalah panel tersebut dibuat menggunakan Komponen Material, yang diharapkan berada dalam MaterialTheme, jika tidak, maka akan kembali ke warna gaya "baseline".

MaterialTheme memiliki setelan default yang baik sehingga tidak mengalami error. Karena kita tidak akan menguji tema atau mengambil screenshot, kita dapat menghilangkannya dan menggunakan tema terang default. Jangan ragu untuk menggabungkan RallyTopAppBar dengan RallyTheme untuk memperbaikinya.

Memverifikasi bahwa tab dipilih

Menemukan elemen UI, memeriksa propertinya, dan melakukan tindakan dilakukan melalui aturan pengujian, dengan mengikuti pola berikut:

composeTestRule{.finder}{.assertion}{.action}

Dalam pengujian ini, Anda akan mencari kata "Akun" untuk memverifikasi bahwa label untuk tab yang dipilih ditampilkan.

baca545ddc8c3fa9.png

Cara yang tepat untuk memahami alat yang tersedia adalah dengan menggunakan Tips Praktis Compose Pengujian Compose atau dokumentasi referensi paket pengujian. Cari pencari dan pernyataan yang dapat membantu sesuai situasi kami. Contoh: onNodeWithText, onNodeWithContentDescription, isSelected, hasContentDescription, assertIsSelected...

Setiap tab memiliki deskripsi konten yang berbeda:

  • Ringkasan
  • Akun
  • Tagihan

Setelah mengetahui hal ini, ganti Thread.sleep(5000) dengan pernyataan yang mencari deskripsi konten dan menegaskan bahwa konten tersebut ada:

import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.onNodeWithContentDescription
...

@Test
fun rallyTopAppBarTest_currentTabSelected() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertIsSelected()
}

Sekarang jalankan kembali pengujian dan Anda akan melihat pengujian hijau:

75bab3b37e795b65.png

Selamat! Anda telah menulis pengujian Compose pertama. Anda telah mempelajari cara menguji secara terpisah, serta cara menggunakan pencari dan pernyataan.

Proses ini cukup sederhana, tetapi memerlukan pengetahuan sebelumnya tentang komponen (deskripsi konten dan properti yang dipilih). Anda akan mempelajari cara memeriksa properti yang tersedia pada langkah berikutnya.

4. Pengujian proses debug

Pada langkah ini, Anda akan memverifikasi bahwa label tab saat ini ditampilkan, dalam huruf besar.

baca545ddc8c3fa9.png

Solusi yang memungkinkan adalah mencoba menemukan teks tersebut dan menegaskan bahwa teks tersebut ada:

import androidx.compose.ui.test.onNodeWithText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase())
        .assertExists()
}

Namun, jika pengujian dijalankan, pengujiannya akan gagal 😱

5755586203324389.png

Pada langkah ini, Anda akan mempelajari cara men-debug-nya menggunakan hierarki semantik.

Hierarki semantik

Pengujian Compose menggunakan struktur yang disebut hierarki semantik untuk mencari elemen di layar dan membaca propertinya. Ini adalah struktur yang juga digunakan layanan aksesibilitas, karena dimaksudkan untuk dibaca oleh layanan seperti TalkBack.

Anda dapat mencetak hierarki Semantik menggunakan fungsi printToLog di node. Tambahkan baris baru ke pengujian:

import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.printToLog
...

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot().printToLog("currentLabelExists")

    composeTestRule
        .onNodeWithText(RallyScreen.Accounts.name.uppercase())
        .assertExists() // Still fails
}

Sekarang jalankan pengujian dan periksa Logcat di Android Studio (Anda dapat mencari currentLabelExists).

...com.example.compose.rally D/currentLabelExists: printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

Pada hierarki Semantik, Anda dapat melihat ada SelectableGroup dengan 3 elemen turunan, yang merupakan tab dari panel aplikasi teratas. Ternyata, tidak ada properti text dengan nilai "ACCOUNTS" dan inilah sebabnya pengujian gagal. Namun, ada deskripsi konten untuk setiap tab. Anda dapat memeriksa cara properti ini ditetapkan dalam composable RallyTab di dalam RallyTopAppBar.kt:

private fun RallyTab(text: String...)
...
    Modifier
        .clearAndSetSemantics { contentDescription = text }

Pengubah ini menghapus properti dari turunan dan menyetel deskripsi kontennya sendiri, sehingga Anda melihat "Accounts" dan bukan "ACCOUNTS".

Ganti pencari onNodeWithText dengan onNodeWithContentDescription, lalu jalankan kembali pengujian:

fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNodeWithContentDescription(RallyScreen.Accounts.name)
        .assertExists()
}

b5a7ae9f8f0ed750.png

Selamat! Anda telah memperbaiki pengujian serta mempelajari ComposeTestRule, pengujian secara terpisah, pencari, pernyataan, dan proses debug dengan hierarki Semantik.

Namun, kabar buruk: pengujian ini tidak terlalu berguna. Jika Anda melihat hierarki Semantik dengan saksama, deskripsi konten ketiga tab ada di sana, terlepas dari apakah tabnya dipilih atau tidak. Kita harus mendalaminya.

5. Menggabungkan dan memisahkan hierarki Semantik

Hierarki Semantik selalu berupaya menjadi seringkas mungkin, hanya menampilkan informasi yang relevan.

Misalnya, dalam TopAppBar, ikon dan label tidak perlu menjadi node yang berbeda. Lihat node "Overview":

120e5327856286cd.png

        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

Node ini memiliki properti (seperti Selected dan Role) yang ditentukan khusus untuk komponen selectable dan deskripsi konten untuk seluruh tab. Ini adalah properti tingkat tinggi, sangat berguna untuk pengujian sederhana. Detail tentang ikon atau teks akan berlebihan sehingga tidak ditampilkan.

Compose akan otomatis menampilkan properti Semantik ini dalam beberapa composable seperti Text. Anda juga dapat menyesuaikan dan menggabungkannya untuk mewakili satu komponen yang terdiri atas satu atau beberapa turunan. Misalnya: Anda dapat mewakili Button yang berisi composable Text. Properti MergeDescendants = 'true' memberi tahu kita bahwa node ini memiliki turunan, tetapi turunan tersebut telah digabungkan ke dalamnya. Dalam pengujian, kita sering kali harus mengakses semua node.

Untuk memverifikasi apakah Text di dalam tab ditampilkan atau tidak, kita dapat mengkueri hierarki Semantik terpisah yang meneruskan useUnmergedTree = true ke pencari onRoot.

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")

}

Output di Logcat sekarang sedikit lebih panjang:

    Printing with useUnmergedTree = 'true'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

Node no. 3 masih belum memiliki turunan:

        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

Namun, node 6, tab yang dipilih, memiliki satu turunan dan sekarang kita dapat melihat properti 'Text':

        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]

Untuk memverifikasi perilaku yang benar sesuai keinginan, Anda akan menulis matcher yang mencari satu node dengan teks "ACCOUNTS" yang induknya adalah node dengan deskripsi konten "Accounts".

Periksa kembali Tips Praktis Pengujian Compose, lalu coba temukan cara untuk menulis matcher tersebut. Perlu diketahui bahwa Anda dapat menggunakan operator boolean seperti and dan or dengan matcher.

Semua pencari memiliki parameter yang disebut useUnmergedTree. Tetapkan parameter tersebut ke true untuk menggunakan hierarki yang dipisahkan.

Coba tulis pengujian tanpa melihat solusinya!

Solusi

import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
...

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNode(
            hasText(RallyScreen.Accounts.name.uppercase()) and
            hasParent(
                hasContentDescription(RallyScreen.Accounts.name)
            ),
            useUnmergedTree = true
        )
        .assertExists()
}

Coba jalankan:

94c57e2cfc12c10b.png

Selamat! Pada langkah ini, Anda telah mempelajari penggabungan properti dan hierarki Semantik yang digabungkan dan dipisahkan.

6. Sinkronisasi

Pengujian apa pun yang Anda tulis harus disinkronkan dengan benar dengan subjek yang diuji. Misalnya, saat Anda menggunakan pencari seperti onNodeWithText, pengujian akan menunggu hingga aplikasi dalam keadaan tidak ada aktivitas sebelum mengkueri hierarki semantik. Tanpa sinkronisasi, pengujian dapat mencari elemen sebelum ditampilkan atau menunggu jika tidak perlu.

Kita akan menggunakan layar Overview untuk langkah ini, yang terlihat seperti ini saat Anda menjalankan aplikasi:

8c467af3570b8de6.gif

Perhatikan animasi yang berulang kali berkedip pada kartu Alerts, yang menarik perhatian ke elemen ini.

Buat kelas pengujian lain yang disebut OverviewScreenTest, lalu tambahkan konten berikut:

package com.example.compose.rally

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.example.compose.rally.ui.overview.OverviewBody
import org.junit.Rule
import org.junit.Test

class OverviewScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun overviewScreen_alertsDisplayed() {
        composeTestRule.setContent {
            OverviewBody()
        }

        composeTestRule
            .onNodeWithText("Alerts")
            .assertIsDisplayed()
    }
}

Jika menjalankan pengujian ini, Anda akan melihat bahwa pengujian tidak pernah selesai (waktunya habis setelah 30 detik).

b2d71bd417326bd3.png

Error menyatakan:

androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.android.ComposeIdlingResource@d075f91

Pada dasarnya, pesan error ini memberi tahu Anda bahwa Compose sibuk secara permanen sehingga tidak ada cara untuk menyinkronkan aplikasi dengan pengujian.

Anda mungkin sudah menebak bahwa masalahnya adalah animasi berkedip tanpa batas. Aplikasi tidak pernah dalam keadaan tidak ada aktivitas sehingga pengujian tidak dapat dilanjutkan.

Mari kita lihat implementasi animasi tanpa batas:

app/src/main/java/com/example/compose/rally/ui/overview/OverviewBody.kt

var currentTargetElevation by remember {  mutableStateOf(1.dp) }
LaunchedEffect(Unit) {
    // Start the animation
    currentTargetElevation = 8.dp
}
val animatedElevation = animateDpAsState(
    targetValue = currentTargetElevation,
    animationSpec = tween(durationMillis = 500),
    finishedListener = {
        currentTargetElevation = if (currentTargetElevation > 4.dp) {
            1.dp
        } else {
            8.dp
        }
    }
)
Card(elevation = animatedElevation.value) { ... }

Kode ini pada dasarnya menunggu hingga animasi selesai (finishedListener), lalu menjalankannya lagi.

Salah satu pendekatan untuk memperbaiki pengujian ini adalah menonaktifkan animasi dalam opsi developer. Ini adalah salah satu cara yang diterima secara luas untuk menanganinya di dunia View.

Di Compose, API animasi didesain dengan mempertimbangkan kemudahan pengujian, sehingga masalah dapat diperbaiki dengan menggunakan API yang benar. Kita dapat menggunakan animasi tanpa batas, bukan memulai ulang animasi animateDpAsState.

Ganti kode di OverviewScreen dengan API yang tepat:

import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.Dp
...

    val infiniteElevationAnimation = rememberInfiniteTransition()
    val animatedElevation: Dp by infiniteElevationAnimation.animateValue(
        initialValue = 1.dp,
        targetValue = 8.dp,
        typeConverter = Dp.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = tween(500),
            repeatMode = RepeatMode.Reverse
        )
    )
    Card(elevation = animatedElevation) {

Jika pengujian dijalankan, pengujiannya akan lulus sekarang:

369e266eed40e4e4.png

Selamat! Pada langkah ini, Anda telah mempelajari sinkronisasi dan cara animasi memengaruhi pengujian.

7. Latihan opsional

Pada langkah ini, Anda akan menggunakan tindakan (lihat Tips Praktis Pengujian) untuk memverifikasi bahwa mengklik tab RallyTopAppBar yang berbeda akan mengubah pilihan.

Petunjuk:

  • Cakupan pengujian harus menyertakan status, yang dimiliki oleh RallyApp.
  • Verifikasikan status, bukan perilaku. Gunakan pernyataan tentang status UI, bukan mengandalkan objek yang telah dipanggil dan caranya.

Tidak ada solusi yang tersedia untuk latihan ini.

8. Langkah berikutnya

Selamat! Anda telah menyelesaikan Pengujian di Jetpack Compose. Sekarang Anda memiliki elemen dasar untuk membuat strategi pengujian yang baik untuk UI Compose.

Jika Anda ingin mempelajari Pengujian dan Compose lebih lanjut, lihat referensi berikut:

  1. Dokumentasi pengujian memiliki informasi selengkapnya tentang pencari, pernyataan, tindakan, dan matcher, serta mekanisme sinkronisasi, manipulasi waktu, dsb.
  2. Bookmark Tips Praktis Pengujian.
  3. Sampel Rally dilengkapi class pengujian screenshot sederhana. Jelajahi file AnimatingCircleTests.kt untuk mempelajarinya lebih lanjut.
  4. Untuk panduan umum tentang pengujian aplikasi Android, Anda dapat mengikuti tiga codelab berikut:
  1. Repositori Sampel Compose di GitHub memiliki beberapa aplikasi dengan pengujian UI.
  2. Jalur Jetpack Compose menampilkan daftar resource untuk membantu Anda memulai Compose.

Selamat mencoba!