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
- Android Studio Terbaru
- Pengetahuan tentang Kotlin
- Pemahaman dasar tentang Compose (seperti anotasi
@Composable
) - Pemahaman dasar tentang pengubah
- Opsional: Pertimbangkan untuk membaca codelab dasar-dasar Jetpack Compose sebelum codelab ini
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:
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:
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()...".
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.
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:
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.
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 😱
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()
}
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":
|-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:
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:
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).
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:
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:
- Dokumentasi pengujian memiliki informasi selengkapnya tentang pencari, pernyataan, tindakan, dan matcher, serta mekanisme sinkronisasi, manipulasi waktu, dsb.
- Bookmark Tips Praktis Pengujian.
- Sampel Rally dilengkapi class pengujian screenshot sederhana. Jelajahi file
AnimatingCircleTests.kt
untuk mempelajarinya lebih lanjut. - Untuk panduan umum tentang pengujian aplikasi Android, Anda dapat mengikuti tiga codelab berikut:
- Repositori Sampel Compose di GitHub memiliki beberapa aplikasi dengan pengujian UI.
- Jalur Jetpack Compose menampilkan daftar resource untuk membantu Anda memulai Compose.
Selamat mencoba!