Android Unit 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.
Pirâmida Pengujian Android #
Android membagi pengujian menjadi dua: src/test/ ( lokal JVM, cepat) dan src/androidTest/ (instrumented, butuh device/emulator). Unit test murni menempati porsi terbesar (70%) karena cepat. UI test paling sedikit (10%) karena lambat.
Berikut program demonstrasi konsep-konsep di atas:
// android_unit_testing.kt
// Mendemonstrasikan konsep pengujian unit Android: pattern Arrange-Act-Assert,
// mock dengan pola manual (tanpa MockK), dan test ViewModel-like.
data class Pengguna(val id: Long, val nama: String, val email: String)
class PenggunaRepository {
private val data = mutableMapOf<Long, Pengguna>()
fun simpan(p: Pengguna) { data[p.id] = p }
fun ambil(id: Long): Pengguna? = data[id]
fun hapus(id: Long): Boolean = data.remove(id) != null
}
class PenggunaViewModel(private val repo: PenggunaRepository) {
var state: String = ""
private set
fun muat(id: Long) {
val p = repo.ambil(id)
state = if (p == null) "kosong" else "${p.nama} <${p.email}>"
}
}
fun main() {
// === Arrange ===
val repo = PenggunaRepository()
repo.simpan(Pengguna(1, "Budi", "[email protected]"))
val vm = PenggunaViewModel(repo)
// === Act ===
vm.muat(1)
// === Assert ===
check(vm.state == "Budi <[email protected]>") { "Expected Budi <[email protected]>, got ${vm.state}" }
println("[LULUS] muat(1) => ${vm.state}")
// === Test 2: pengguna tidak ada ===
vm.muat(99)
check(vm.state == "kosong") { "Expected kosong, got ${vm.state}" }
println("[LULUS] muat(99) => ${vm.state}")
// === Test 3: hapus ===
val deleted = repo.hapus(1)
check(deleted) { "Expected true, got $deleted" }
println("[LULUS] hapus(1) => true")
val deletedAgain = repo.hapus(1)
check(!deletedAgain) { "Expected false, got $deletedAgain" }
println("[LULUS] hapus(1) lagi => false")
println("\nSemua unit test lulus. Pola: Arrange-Act-Assert, isolasi dependency via constructor injection.")
}Output yang Dihasilkan:
[LULUS] muat(1) => Budi <[email protected]>
[LULUS] muat(99) => kosong
[LULUS] hapus(1) => true
[LULUS] hapus(1) lagi => false
Semua unit test lulus. Pola: Arrange-Act-Assert, isolasi dependency via constructor injection.Test Lokal dengan JVM #
Test di src/test/ berjalan di JVM tanpa Android framework. Cocok untuk ViewModel, Repository, UseCase yang tidak bergantung pada Context. Gunakan kotlin.test atau JUnit 4/5. Dependensi framework Android harus di-mock.
Mock dengan MockK #
MockK adalah library mocking yang idiomatic Kotlin. mockk<MyRepo>(), every { repo.fetch() } returns data, verify { view.showData() }. Mendukung coroutine, suspend function, dan extension function.
TestState dan TestScope #
Untuk ViewModel yang menggunakan viewModelScope, gunakan kotlinx-coroutines-test dengan runTest. TestDispatcher mengontrol virtual time — delay(100) selesai instan.
Code Coverage #
JaCoCo mengukur cakupan bytecode. ./gradlew jacocoTestReport menghasilkan laporan HTML. Target awal 60% line coverage, naik bertahap. Fokus pada path kritis (ViewModel, Repository).
Test Doubles untuk Android #
Test doubles adalah pengganti dependency nyata yang membuat test cepat dan deterministik. Empat varian utama yang sering dijumpai adalah stub, mock, fake, dan spy, masing-masing dengan tujuan berbeda. Pemilihan jenis double yang tepat menentukan kualitas dan kecepatan eksekusi test.
Stub mengembalikan nilai tetap tanpa logika, cocok untuk kasus sederhana yang hanya butuh satu jawaban. Mock merekam interaksi dan memverifikasi bahwa method tertentu dipanggil, cocok untuk memastikan alur event. Fake menyimpan state sederhana dan meniru perilaku asli, cocok untuk repository atau data source dalam test. Spy membungkus object nyata dan mengamati sebagian interaksi, cocok untuk audit tanpa mengganti seluruh implementasi.
// Stub: nilai tetap
class StubUserApi : UserApi {
override fun fetch(id: Long) = User(id, "Budi")
}
// Fake: implementasi ringan dengan state
class FakeUserRepo : UserRepo {
private val data = mutableMapOf<Long, User>()
override fun simpan(u: User) { data[u.id] = u }
override fun ambil(id: Long) = data[id]
}
// Mock dengan MockK
val mockApi = mockk<UserApi>()
every { mockApi.fetch(1) } returns User(1, "Siti")
verify { mockApi.fetch(1) }Untuk pengujian HTTP di Android, MockWebServer dari OkHttp adalah pilihan standar. Server tiruan ini menerima MockResponse yang mengembalikan status code dan body yang sudah ditentukan, sehingga test tidak bergantung pada koneksi internet nyata. Kombinasikan MockWebServer dengan Retrofit atau Ktor client yang dikonfigurasi menuju URL server tiruan tersebut.
Coroutine Test Pattern #
Pengujian ViewModel yang menggunakan viewModelScope membutuhkan kontrol penuh atas dispatcher. Library kotlinx-coroutines-test menyediakan runTest yang menjalankan test dalam TestScope dengan virtual time. delay(1000) selesai instan karena clock diatur oleh StandardTestDispatcher, sehingga test yang menunggu animasi panjang tetap cepat dieksekusi.
Pola dasar: ganti Dispatchers.Main dengan TestDispatcher melalui Dispatchers.setMain(testDispatcher). Lalu, semua coroutine yang dijalankan dalam viewModelScope akan otomatis berjalan pada dispatcher tersebut. Setelah test selesai, panggil Dispatchers.resetMain() di blok @After agar dispatcher global kembali ke kondisi semula dan tidak bocor ke test berikutnya.
class UserViewModelTest {
private val dispatcher = StandardTestDispatcher()
private val scope = TestScope(dispatcher)
@Before fun setUp() {
Dispatchers.setMain(dispatcher)
}
@After fun tearDown() {
Dispatchers.resetMain()
}
@Test fun memuat_data_dari_repository() = scope.runTest {
val vm = UserViewModel(FakeUserRepo())
vm.muat(1)
advanceUntilIdle() // jalankan semua coroutine yang tertunda
assertEquals("Budi", vm.state.nama)
}
}Fungsi advanceUntilIdle() memajukan virtual clock hingga semua coroutine selesai. Untuk verifikasi state tengah, gunakan runCurrent() setelah memicu action tanpa menunggu seluruhnya selesai. Pola ini menjaga test tetap cepat dan tidak bergantung pada Thread.sleep yang rawan false positive pada mesin CI yang lambat.
Referensi Lanjutan #
- Testing fundamentals — panduan resmi pengujian aplikasi Android mencakup unit test, instrumented test, dan UI test
Penutup #
Dengan memahami android unit 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.