Compose UI Testing adalah topik penting dalam pengembangan aplikasi Android dengan Kotlin. Memahaminya membantu kita membangun aplikasi yang andal, mudah diuji, dan siap untuk publikasi ke Google Play Store. Pada chapter ini kita akan membongkar konsep inti, melihat contoh kode praktis, dan menutup dengan checklist implementasi.

Setup Test #

Tambah dependency androidx.compose.ui:ui-test-junit4. Annotate test dengan @RunWith(AndroidJUnit4::class). Gunakan createComposeRule() atau createAndroidComposeRule<MainActivity>().

Berikut program demonstrasi konsep-konsep di atas:

kotlin
// compose_ui_testing.kt
// Mendemonstrasikan konsep pengujian UI Compose: findNode, performClick,
// dan verifikasi state — disimulasikan dengan struktur data Kotlin murni.

sealed class LoginState {
    object Idle : LoginState()
    data class Loading(val pesan: String) : LoginState()
    data class Success(val token: String) : LoginState()
    data class Error(val pesan: String) : LoginState()
}

class LoginScreen {
    var email: String = ""
    var password: String = ""
    var state: LoginState = LoginState.Idle
        private set

    fun onEmailChange(v: String) { email = v }
    fun onPasswordChange(v: String) { password = v }
    fun onSubmit() {
        state = if (email.isBlank() || password.isBlank()) {
            LoginState.Error("Email dan password wajib diisi")
        } else if (password.length < 6) {
            LoginState.Error("Password minimal 6 karakter")
        } else {
            LoginState.Loading("Mengirim...")
        }
    }
    fun selesaikan(token: String) { state = LoginState.Success(token) }
}

fun main() {
    // === Test 1: awalnya idle ===
    val screen = LoginScreen()
    check(screen.state is LoginState.Idle) { "Expected Idle" }
    println("[LULUS] state awal: Idle")

    // === Test 2: submit kosong menampilkan error ===
    screen.onSubmit()
    val errState = screen.state
    check(errState is LoginState.Error && errState.pesan.contains("wajib diisi")) { "Expected error wajib diisi" }
    println("[LULUS] submit kosong: ${errState as LoginState.Error}")

    // === Test 3: password pendek ===
    screen.onEmailChange("[email protected]")
    screen.onPasswordChange("123")
    screen.onSubmit()
    val errState2 = screen.state
    check(errState2 is LoginState.Error && errState2.pesan.contains("6 karakter")) { "Expected error 6 karakter" }
    println("[LULUS] submit password pendek: ${errState2 as LoginState.Error}")

    // === Test 4: input valid → loading → success ===
    screen.onPasswordChange("password-benar")
    screen.onSubmit()
    check(screen.state is LoginState.Loading) { "Expected Loading" }
    println("[LULUS] submit valid: ${screen.state as LoginState.Loading}")

    screen.selesaikan("token-abc-123")
    check(screen.state is LoginState.Success) { "Expected Success" }
    println("[LULUS] selesaikan: ${screen.state as LoginState.Success}")

    println("\nCompose UI test: temukan node (state), trigger action (onSubmit), verifikasi state baru.")
}

Output yang Dihasilkan:

text
[LULUS] state awal: Idle
[LULUS] submit kosong: Error(pesan=Email dan password wajib diisi)
[LULUS] submit password pendek: Error(pesan=Password minimal 6 karakter)
[LULUS] submit valid: Loading(pesan=Mengirim...)
[LULUS] selesaikan: Success(token=token-abc-123)

Compose UI test: temukan node (state), trigger action (onSubmit), verifikasi state baru.

Mencari Node #

onNodeWithText("Login"), onNodeWithTag("emailField"), onAllNodesWithContentDescription("tombol simpan"). Assertion: .assertIsDisplayed(), .assertIsEnabled(), .assertTextEquals("Halo").

Aksi #

.performClick(), .performTextInput("[email protected]"), .performScrollTo(). Aksi memicu recomposition; assertion berikutnya menunggu hingga idle.

Screenshot Testing #

Library seperti shot atau paparazzi membandingkan screenshot dengan baseline. Berguna untuk regression visual. Simpan baseline di repo, fail test bila pixel berubah.

Test State #

Pola: expose MutableState ViewModel di test, eksekusi action, verifikasi state berubah. Hindari sleep — Compose test otomatis menunggu idle.

Pola AAA di Compose Test #

Pola Arrange-Act-Assert (AAA) membantu kita menyusun test Compose yang mudah dibaca dan dirawat. Tahap Arrange menyiapkan state awal ViewModel, fake repository, atau modul dependency injection yang dikonfigurasi khusus sebelum composable dirender. Tahap Act memicu interaksi pengguna melalui aksi klik, input teks, atau scroll pada node yang dipilih. Tahap Assert memverifikasi tampilan melalui matcher semantik yang memeriksa teks, posisi, atau status enabled pada node.

Pemisahan tiga tahap membuat kegagalan test lebih mudah dilacak. Bila assertion gagal, kita tahu penyebabnya terletak pada Act atau Arrange, bukan campuran keduanya. Pemisahan ini juga membantu pembaca memahami maksud test dalam sekali baca tanpa harus menelusuri baris kode secara linear.

Hindari menumpuk beberapa skenario dalam satu test. Bila test tunggal memeriksa input kosong, password pendek, dan login sukses sekaligus, satu kegagalan akan menyembunyikan kegagalan lain. Lebih baik pecah menjadi tiga test terpisah, masing-masing mengikuti pola AAA yang utuh. Beri nama test yang menyatakan skenario dan ekspektasi, misalnya login_denganEmailKosong_menampilkanPesanWajib.

kotlin Pola AAA pada Compose test
// Arrange: siapkan ViewModel dengan repository palsu
val rule = createComposeRule()
val vm = LoginViewModel(FakeAuthRepo())

// Act: render, isi form, klik tombol
rule.setContent { LoginScreen(viewModel = vm) }
rule.onNodeWithTag("email").performTextInput("[email protected]")
rule.onNodeWithTag("password").performTextInput("password-benar")
rule.onNodeWithText("Login").performClick()

// Assert: verifikasi pesan selamat datang muncul
rule.onNodeWithText("Selamat datang").assertIsDisplayed()

Bila test membutuhkan precondition yang kompleks, pindahkan setup ke method @Before agar tiap test dimulai dari kondisi bersih. Hal ini menjaga isolasi antar test dan mencegah state bocor dari test sebelumnya. Untuk test yang melibatkan navigation, gunakan TestNavHostController agar transisi antar destinasi bisa diuji tanpa membungkus seluruh aplikasi.

Test State Lifecycle #

Compose test runner memiliki mekanisme menunggu recomposition secara otomatis. Setelah aksi pengguna dijalankan, runner memanggil waitForIdle() internal sebelum assertion berikutnya dieksekusi. Kondisi idle tercapai ketika semua snapshot state stabil, animasi selesai, dan tidak ada pekerjaan asinkron yang masih berjalan pada clock test.

Untuk komponen yang memuat data dari network atau database, gunakan IdlingResource agar runner tahu kapan proses latar belakang selesai. Tanpa IdlingResource, test bisa memeriksa state sebelum data tiba dan menghasilkan false negative yang tidak konsisten antar mesin. Pendaftaran IdlingResource dilakukan sebelum test berjalan dan wajib dilepas setelah test selesai.

kotlin Pendaftaran IdlingResource
class DataLoadIdlingResource(
    private val counting: CountingIdlingResource
) : IdlingResource {
    override val isIdleNow: Boolean get() = counting.isIdleNow
    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        counting.registerIdleTransitionCallback { callback.onTransitionToIdle() }
    }
}

// Di dalam test:
val idling = DataLoadIdlingResource(EspressoIdlingResource.counting)
composeTestRule.registerIdlingResource(idling)
composeTestRule.onNodeWithText("Budi").assertIsDisplayed()
composeTestRule.unregisterIdlingResource(idling)

Bila test memicu animasi panjang, nonaktifkan mainClock.autoAdvance lalu majukan waktu manual melalui advanceTimeBy. Pendekatan ini menjaga test tetap deterministik pada mesin CI yang lambat. Gunakan pola ini juga saat ingin memverifikasi frame animasi tengah, misalnya untuk memastikan loading spinner tampil sebelum data benar-benar dimuat.

Referensi Lanjutan #

Penutup #

Dengan memahami compose ui testing, kita siap membangun aplikasi Android yang lebih matang dan siap produksi. Materi terkait di section lain meliputi coroutines, Compose Multiplatform, dan testing — semuanya saling melengkapi untuk pengembangan Android modern.