Menguji tata letak Compose Anda

Pengujian UI atau layar digunakan untuk memverifikasi perilaku kode Compose yang benar, yang meningkatkan kualitas aplikasi dengan menangkap error lebih awal dalam proses pengembangan.

Compose menyediakan sekumpulan API pengujian untuk menemukan elemen, memverifikasi atributnya, dan melakukan tindakan pengguna. Fitur ini juga mencakup fitur lanjutan seperti manipulasi waktu.

Semantik

Pengujian UI di Compose menggunakan semantik untuk berinteraksi dengan hierarki UI. Semantik, sesuai namanya, memberikan makna pada UI. Dalam konteks ini, "bagian UI" (atau elemen) dapat berarti apa saja, dari satu composable ke layar penuh. Hierarki semantik dibuat bersama hierarki UI dan menjelaskannya.

Diagram yang menunjukkan tata letak UI standar, dan cara tata letak itu akan dipetakan ke pohon semantik yang sesuai

Gambar 1. Hierarki UI standar dan pohon semantiknya.

Framework semantik terutama digunakan untuk aksesibilitas, sehingga pengujian memanfaatkan informasi yang diekspos oleh semantik tentang hierarki UI. Developer menentukan apa dan seberapa banyak yang diekspos.

Tombol yang berisi gambar dan teks

Gambar 2. Tombol standar yang berisi ikon dan teks.

Misalnya, dengan tombol seperti ini yang terdiri dari ikon dan elemen teks, hierarki semantik default hanya berisi label teks "Like". Ini karena beberapa composable, seperti Text, telah mengekspos beberapa properti ke pohon semantik. Anda dapat menambahkan properti ke pohon semantik menggunakan Modifier.

MyButton(
    modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)

Penyiapan

Bagian ini menjelaskan cara menyiapkan modul untuk memungkinkan Anda menguji kode komposisi.

Pertama, tambahkan dependensi berikut ke file build.gradle modul yang berisi pengujian UI Anda:

// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createComposeRule(), but not for createAndroidComposeRule<YourActivity>():
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Modul ini mencakup ComposeTestRule dan implementasi untuk Android yang disebut AndroidComposeTestRule. Melalui aturan ini, Anda dapat menetapkan konten Compose atau mengakses aktivitas. Aturan ini dibuat menggunakan fungsi factory createComposeRule, atau createAndroidComposeRule jika Anda memerlukan akses ke aktivitas. Pengujian UI standar untuk Compose terlihat seperti ini:

// file: app/src/androidTest/java/com/package/MyComposeTest.kt

class MyComposeTest {

    @get:Rule val composeTestRule = createComposeRule()
    // use createAndroidComposeRule<YourActivity>() if you need access to
    // an activity

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = fakeUiState, /*...*/)
            }
        }

        composeTestRule.onNodeWithText("Continue").performClick()

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

API Pengujian

Ada tiga cara utama untuk berinteraksi dengan elemen:

  • Pencari memungkinkan Anda untuk memilih satu atau beberapa elemen (atau node dalam pohon Semantik) untuk membuat pernyataan atau melakukan tindakan pada elemen tersebut.
  • Pernyataan digunakan untuk memverifikasi bahwa elemen tersebut ada atau memiliki atribut tertentu.
  • Tindakan memasukkan simulasi peristiwa pengguna ke dalam elemen, seperti klik atau gestur lainnya.

Beberapa API ini akan menerima SemanticsMatcher untuk merujuk ke satu atau beberapa node dalam pohon semantik.

Pencari

Anda dapat menggunakan onNode dan onAllNodes untuk memilih satu atau beberapa node, tetapi Anda juga dapat menggunakan pencari praktis untuk penelusuran yang paling umum, seperti onNodeWithText , onNodeWithContentDescription, dll. Anda dapat menjelajahi daftar lengkap dalam Tips praktis Pengujian Compose.

Memilih node tunggal

composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
    .onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")

Memilih beberapa node

composeTestRule
    .onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
    .onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")

Menggunakan pohon yang terpisah

Beberapa node menggabungkan informasi semantik turunannya. Misalnya, tombol dengan dua elemen teks akan menggabungkan labelnya:

MyButton {
    Text("Hello")
    Text("World")
}

Dari pengujian, kita dapat menggunakan printToLog() untuk menampilkan pohon semantik:

composeTestRule.onRoot().printToLog("TAG")

Kode ini mencetak output berikut:

Node #1 at (...)px
 |-Node #2 at (...)px
   Role = 'Button'
   Text = '[Hello, World]'
   Actions = [OnClick, GetTextLayoutResult]
   MergeDescendants = 'true'

Jika Anda harus mencocokkan node dari pohon terpisah, Anda dapat menetapkan useUnmergedTree ke true:

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

Kode ini mencetak output berikut:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = '[Hello]'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = '[World]'

Parameter useUnmergedTree tersedia di semua pencari. Misalnya, di sini digunakan dalam pencari onNodeWithText.

composeTestRule
    .onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()

Pernyataan

Periksa pernyataan dengan memanggil assert() pada SemanticsNodeInteraction yang ditampilkan oleh pencari dengan satu atau beberapa matcher:

// Single matcher:
composeTestRule
    .onNode(matcher)
    .assert(hasText("Button")) // hasText is a SemanticsMatcher

// Multiple matchers can use and / or
composeTestRule
    .onNode(matcher).assert(hasText("Button") or hasText("Button2"))

Anda juga dapat menggunakan fungsi praktis untuk pernyataan paling umum, seperti assertExists , assertIsDisplayed , assertTextEquals , dll. Anda dapat melihat daftar lengkapnya di Tips Praktis Pengujian Compose.

Ada juga fungsi untuk memeriksa pernyataan pada kumpulan node:

// Check number of matched nodes
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
    .onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())

Tindakan

Untuk memasukkan tindakan pada node, panggil fungsi perform…():

composeTestRule.onNode(...).performClick()

Berikut ini beberapa contoh tindakan:

performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }

Anda dapat menjelajahi daftar lengkap di Tips praktis Pengujian Compose.

Matcher

Bagian ini menjelaskan beberapa matcher yang tersedia untuk menguji kode Compose Anda.

Matcher hierarki

Matcher hierarki memungkinkan Anda naik atau turun pohon semantik dan melakukan pencocokan sederhana.

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

Berikut ini beberapa contoh matcher yang digunakan:

composeTestRule.onNode(hasParent(hasText("Button")))
    .assertIsDisplayed()

Pemilih

Cara alternatif untuk membuat pengujian adalah dengan menggunakan pemilih yang dapat membuat beberapa pengujian lebih mudah dibaca.

composeTestRule.onNode(hasTestTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

Anda dapat menjelajahi daftar lengkap di Tips praktis Pengujian Compose.

Sinkronisasi

Uji Compose disinkronkan secara default dengan UI Anda. Saat Anda memanggil pernyataan atau tindakan melalui 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()
}

Penting juga untuk diperhatikan 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 tidak ada aktivitas

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.

Anda dapat membuat dan mendaftarkan 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 Resource Tidak Ada Aktivitas 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.

waitForIdle menunggu Compose menjadi tidak ada aktivitas, tetapi 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 akan 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).

Menunggu kondisi

Kondisi apa pun yang bergantung pada pekerjaan eksternal, seperti pemuatan data atau ukuran atau penggambaran Android (yaitu, mengukur atau menggambar eksternal ke Compose), harus menggunakan konsep yang lebih umum seperti waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Anda juga dapat menggunakan salah satu helper waitUntil:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Pola umum

Bagian ini menjelaskan beberapa pendekatan umum yang akan Anda lihat dalam pengujian Compose.

Pengujian secara terpisah

ComposeTestRule memungkinkan Anda memulai aktivitas yang menampilkan composable apa pun: aplikasi lengkap, satu layar, atau elemen kecil. Ada baiknya juga untuk memeriksa apakah composable Anda dienkapsulasi dengan benar dan berfungsi secara independen, sehingga memungkinkan pengujian UI yang lebih mudah dan lebih terfokus.

Ini tidak berarti Anda hanya akan membuat pengujian UI unit. Pengujian UI yang mencakup sebagian besar UI yang ada juga sangat penting.

Mengakses aktivitas dan resource setelah menetapkan konten Anda sendiri

Sering kali Anda perlu menetapkan konten yang sedang diuji menggunakan composeTestRule.setContent dan juga perlu mengakses resource aktivitas, misalnya untuk menyatakan bahwa teks yang ditampilkan cocok dengan resource string. Namun, Anda tidak dapat memanggil setContent pada aturan yang dibuat dengan createAndroidComposeRule() jika aktivitas sudah memanggilnya.

Pola umum untuk melakukannya adalah dengan membuat AndroidComposeTestRule menggunakan aktivitas kosong (seperti ComponentActivity).

class MyComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun myTest() {
        // Start the app
        composeTestRule.setContent {
            MyAppTheme {
                MainScreen(uiState = exampleUiState, /*...*/)
            }
        }
        val continueLabel = composeTestRule.activity.getString(R.string.next)
        composeTestRule.onNodeWithText(continueLabel).performClick()
    }
}

Perlu diperhatikan bahwa ComponentActivity harus ditambahkan ke file AndroidManifest.xml aplikasi Anda. Anda dapat melakukannya dengan menambahkan dependensi ini ke modul:

debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

Properti semantik kustom

Anda dapat membuat properti semantik khusus untuk mengekspos informasi pada pengujian. Untuk melakukannya, tentukan SemanticsPropertyKey baru dan sediakan SemanticsPropertyReceiver.

// Creates a Semantics property of type Long
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey

Sekarang Anda dapat menggunakan properti tersebut menggunakan pengubah semantics:

val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
    modifier = Modifier.semantics { pickedDate = datePickerValue }
)

Dari pengujian, Anda dapat menggunakan SemanticsMatcher.expectValue untuk menyatakan nilai properti:

composeTestRule
    .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
    .assertExists()

Memverifikasi pemulihan status

Anda harus memverifikasi bahwa status elemen Compose dipulihkan dengan benar saat aktivitas atau proses dibuat ulang. Anda dapat melakukan pemeriksaan tersebut tanpa mengandalkan pembuatan ulang aktivitas dengan class StateRestorationTester.

Class ini memungkinkan Anda menyimulasikan pembuatan ulang composable. Hal ini sangat berguna untuk memverifikasi penerapan rememberSaveable.


class MyStateRestorationTests {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun onRecreation_stateIsRestored() {
        val restorationTester = StateRestorationTester(composeTestRule)

        restorationTester.setContent { MainScreen() }

        // TODO: Run actions that modify the state

        // Trigger a recreation
        restorationTester.emulateSavedInstanceStateRestore()

        // TODO: Verify that state has been correctly restored.
    }
}

Menguji berbagai konfigurasi perangkat

Aplikasi Android harus beradaptasi dengan berbagai kondisi yang berubah: ukuran jendela, lokalitas, ukuran font, tema gelap dan terang, serta banyak lagi. Sebagian besar kondisi ini berasal dari nilai tingkat perangkat yang dikontrol oleh pengguna dan diekspos dengan instance Configuration saat ini. Menguji berbagai konfigurasi secara langsung dalam pengujian sulit karena pengujian harus mengonfigurasi properti tingkat perangkat.

DeviceConfigurationOverride adalah API khusus pengujian yang memungkinkan Anda menyimulasikan berbagai konfigurasi perangkat dengan cara yang dilokalkan untuk konten @Composable yang sedang diuji.

Objek pendamping DeviceConfigurationOverride memiliki fungsi ekstensi berikut, yang mengganti properti konfigurasi tingkat perangkat:

Untuk menerapkan penggantian tertentu, gabungkan konten yang sedang diuji dalam panggilan ke fungsi tingkat atas DeviceConfigurationOverride(), dengan meneruskan penggantian untuk diterapkan sebagai parameter.

Misalnya, kode berikut menerapkan penggantian DeviceConfigurationOverride.ForcedSize() untuk mengubah kepadatan secara lokal, yang memaksa composable MyScreen dirender di jendela lanskap besar, meskipun perangkat tempat pengujian berjalan tidak mendukung ukuran jendela tersebut secara langsung:

composeTestRule.setContent {
    DeviceConfigurationOverride(
        DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
    ) {
        MyScreen() // will be rendered in the space for 1280dp by 800dp without clipping
    }
}

Untuk menerapkan beberapa penggantian secara bersamaan, gunakan DeviceConfigurationOverride.then():

composeTestRule.setContent {
    DeviceConfigurationOverride(
        DeviceConfigurationOverride.FontScale(1.5f) then
            DeviceConfigurationOverride.FontWeightAdjustment(200)
    ) {
        Text(text = "text with increased scale and weight")
    }
}

Proses Debug

Cara utama untuk menyelesaikan masalah dalam pengujian Anda adalah dengan melihat pohon semantik. Anda dapat mencetak pohon dengan memanggil composeTestRule.onRoot().printToLog() kapan saja dalam pengujian Anda. Fungsi ini mencetak log seperti ini:

Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = 'Hi'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = 'There'

Log ini berisi informasi berharga untuk melacak bug.

Interoperabilitas dengan Espresso

Dalam aplikasi hybrid, Anda dapat menemukan komponen Compose di hierarki tampilan dan tampilan di dalam composable Compose (melalui composable AndroidView).

Tidak memerlukan langkah khusus untuk mencocokkan salah satu jenisnya. Anda mencocokkan tampilan melalui onView Espresso, dan elemen Compose melalui ComposeTestRule.

@Test
fun androidViewInteropTest() {
    // Check the initial state of a TextView that depends on a Compose state:
    Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
    // Click on the Compose button that changes the state
    composeTestRule.onNodeWithText("Click here").performClick()
    // Check the new value
    Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}

Interoperabilitas dengan UiAutomator

Secara default, composable dapat diakses dari UiAutomator hanya dengan deskripsi yang praktis (teks yang ditampilkan, deskripsi konten, dll.). Jika ingin mengakses semua composable yang menggunakan Modifier.testTag, Anda harus mengaktifkan properti semantik testTagsAsResourceId untuk subhierarki composable tertentu. Mengaktifkan perilaku ini berguna untuk composable yang tidak memiliki handle unik lainnya, seperti composable yang dapat di-scroll (misalnya, LazyColumn).

Anda dapat mengaktifkannya hanya sekali di hierarki composable untuk memastikan semua composable bertingkat dengan Modifier.testTag dapat diakses dari UiAutomator.

Scaffold(
    // Enables for all composables in the hierarchy.
    modifier = Modifier.semantics {
        testTagsAsResourceId = true
    }
){
    // Modifier.testTag is accessible from UiAutomator for composables nested here.
    LazyColumn(
        modifier = Modifier.testTag("myLazyColumn")
    ){
        // content
    }
}

Setiap composable dengan Modifier.testTag(tag) dapat diakses dengan penggunaan By.res(resourceName) menggunakan tag yang sama seperti resourceName.

val device = UiDevice.getInstance(getInstrumentation())

val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn

Pelajari lebih lanjut

Untuk mempelajari lebih lanjut, coba Codelab Pengujian Jetpack Compose.

Contoh