Codelab ini adalah bagian dari kursus Android Lanjutan di Kotlin. Anda akan mendapatkan manfaat maksimal dari kursus ini jika menyelesaikan codelab secara berurutan, tetapi ini tidak bersifat wajib. Semua codelab kursus tercantum di halaman landing codelab Android Lanjutan di Kotlin.
Pengantar
Codelab pengujian kedua ini membahas semua hal tentang dummy pengujian: kapan harus menggunakannya di Android, dan cara menerapkannya menggunakan injeksi dependensi, pola Pencari Layanan, dan library. Dengan melakukannya, Anda akan mempelajari cara menulis:
- Pengujian unit repositori
- Pengujian integrasi fragmen dan viewmodel
- Pengujian navigasi fragmen
Yang harus sudah Anda ketahui
Anda harus memahami:
- Bahasa pemrograman Kotlin
- Konsep pengujian yang dibahas dalam codelab pertama: Menulis dan menjalankan pengujian unit di Android, menggunakan JUnit, Hamcrest, pengujian AndroidX, Robolectric, serta Pengujian LiveData
- Library Android Jetpack inti berikut:
ViewModel
,LiveData
, dan Komponen Navigasi - Arsitektur aplikasi, mengikuti pola dari Panduan arsitektur aplikasi dan codelab Dasar-Dasar Android
- Dasar-dasar coroutine di Android
Yang akan Anda pelajari
- Cara merencanakan strategi pengujian
- Cara membuat dan menggunakan duplikat pengujian, yaitu tiruan dan tiruan objek
- Cara menggunakan injeksi dependensi manual di Android untuk pengujian unit dan integrasi
- Cara menerapkan Pola Pencari Layanan
- Cara menguji repositori, fragmen, ViewModel, dan komponen Navigasi
Anda akan menggunakan library dan konsep kode berikut:
Yang akan Anda lakukan
- Menulis pengujian unit untuk repositori menggunakan test double dan injeksi dependensi.
- Menulis pengujian unit untuk model tampilan menggunakan test double dan injeksi dependensi.
- Tulis pengujian integrasi untuk fragmen dan model tampilannya menggunakan framework pengujian UI Espresso.
- Tulis pengujian navigasi menggunakan Mockito dan Espresso.
Dalam serangkaian codelab ini, Anda akan mengerjakan aplikasi TO-DO Notes. Aplikasi ini memungkinkan Anda menuliskan tugas yang harus diselesaikan dan menampilkannya dalam daftar. Kemudian, Anda dapat menandainya sebagai selesai atau tidak, memfilternya, atau menghapusnya.
Aplikasi ini ditulis di Kotlin, memiliki beberapa layar, menggunakan komponen Jetpack, dan mengikuti arsitektur dari Panduan arsitektur aplikasi. Dengan mempelajari cara menguji aplikasi ini, Anda akan dapat menguji aplikasi yang menggunakan library dan arsitektur yang sama.
Download Kode
Untuk memulai, download kode:
Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
Luangkan waktu sejenak untuk memahami kode, dengan mengikuti petunjuk di bawah.
Langkah 1: Jalankan aplikasi contoh
Setelah Anda mendownload aplikasi TO-DO, buka di Android Studio dan jalankan. Kode harus dikompilasi. Jelajahi aplikasi dengan melakukan hal berikut:
- Buat tugas baru dengan tombol tindakan mengambang plus. Masukkan judul terlebih dahulu, lalu masukkan informasi tambahan tentang tugas. Simpan dengan FAB centang hijau.
- Dalam daftar tugas, klik judul tugas yang baru saja Anda selesaikan dan lihat layar detail tugas tersebut untuk melihat deskripsi lainnya.
- Dalam daftar atau di layar detail, centang kotak tugas tersebut untuk menyetel statusnya ke Selesai.
- Kembali ke layar tugas, buka menu filter, dan filter tugas menurut status Aktif dan Selesai.
- Buka panel navigasi, lalu klik Statistik.
- Kembali ke layar ringkasan, dan dari menu panel navigasi, pilih Hapus selesai untuk menghapus semua tugas dengan status Selesai
Langkah 2: Pelajari kode aplikasi contoh
Aplikasi TO-DO didasarkan pada sampel pengujian dan arsitektur Architecture Blueprints yang populer (menggunakan sampel versi reactive architecture). Aplikasi mengikuti arsitektur dari Panduan arsitektur aplikasi. Aplikasi ini menggunakan ViewModel dengan Fragmen, repositori, dan Room. Jika Anda sudah terbiasa dengan salah satu contoh di bawah, aplikasi ini memiliki arsitektur yang serupa:
- Room dengan Codelab View
- Codelab pelatihan Dasar-Dasar Android Kotlin
- Codelab pelatihan Android Lanjutan
- Contoh Android Sunflower
- Kursus pelatihan Udacity Developing Android Apps with Kotlin
Lebih penting bagi Anda untuk memahami arsitektur umum aplikasi daripada memiliki pemahaman mendalam tentang logika di salah satu lapisan.
Berikut ringkasan paket yang akan Anda temukan:
Paket: | |
| Layar menambahkan atau mengedit tugas: Kode lapisan UI untuk menambahkan atau mengedit tugas. |
| Lapisan data: Bagian ini berhubungan dengan lapisan data tugas. Direktori ini berisi kode database, jaringan, dan repositori. |
| Layar statistik: Kode lapisan UI untuk layar statistik. |
| Layar detail tugas: Kode lapisan UI untuk satu tugas. |
| Layar tugas: Kode lapisan UI untuk daftar semua tugas. |
| Class utilitas: Class bersama yang digunakan di berbagai bagian aplikasi, misalnya untuk tata letak tarik lalu lepas yang digunakan di beberapa layar. |
Lapisan data (.data)
Aplikasi ini menyertakan lapisan jaringan simulasi, dalam paket remote, dan lapisan database, dalam paket local. Agar sederhana, dalam project ini, lapisan jaringan disimulasikan hanya dengan HashMap
dengan penundaan, bukan membuat permintaan jaringan yang sebenarnya.
DefaultTasksRepository
mengoordinasikan atau memediasi antara lapisan jaringan dan lapisan database, serta menampilkan data ke lapisan UI.
Lapisan UI ( .addedittask, .statistics, .taskdetail, .tasks)
Setiap paket lapisan UI berisi fragmen dan model tampilan, beserta class lain yang diperlukan untuk UI (seperti adapter untuk daftar tugas). TaskActivity
adalah aktivitas yang berisi semua fragmen.
Navigasi
Navigasi untuk aplikasi dikontrol oleh komponen Navigation. Hal ini ditentukan dalam file nav_graph.xml
. Navigasi dipicu di model tampilan menggunakan class Event
; model tampilan juga menentukan argumen yang akan diteruskan. Fragmen mengamati Event
dan melakukan navigasi sebenarnya antar-layar.
Dalam codelab ini, Anda akan mempelajari cara menguji repositori, model tampilan, dan fragmen menggunakan test double dan injeksi dependensi. Sebelum mempelajari lebih lanjut apa saja pengujian tersebut, penting untuk memahami alasan yang akan memandu apa dan bagaimana Anda akan menulis pengujian ini.
Bagian ini membahas beberapa praktik terbaik pengujian secara umum, sebagaimana berlaku untuk Android.
Piramida Pengujian
Saat memikirkan strategi pengujian, ada tiga aspek pengujian terkait:
- Cakupan—Seberapa banyak kode yang diuji? Pengujian dapat dijalankan pada satu metode, di seluruh aplikasi, atau di antara keduanya.
- Kecepatan—Seberapa cepat pengujian berjalan? Kecepatan pengujian dapat bervariasi dari milidetik hingga beberapa menit.
- Fidelitas—Seberapa "nyata" pengujian ini? Misalnya, jika bagian kode yang Anda uji perlu membuat permintaan jaringan, apakah kode pengujian benar-benar membuat permintaan jaringan ini, atau apakah kode tersebut memalsukan hasilnya? Jika pengujian benar-benar berkomunikasi dengan jaringan, berarti pengujian tersebut memiliki keakuratan yang lebih tinggi. Kelemahannya adalah pengujian dapat memerlukan waktu lebih lama untuk dijalankan, dapat menyebabkan error jika jaringan tidak berfungsi, atau dapat mahal untuk digunakan.
Ada kompromi yang melekat di antara aspek-aspek ini. Misalnya, kecepatan dan akurasi adalah pertukaran—semakin cepat pengujian, umumnya semakin rendah akurasinya, dan sebaliknya. Salah satu cara umum untuk membagi pengujian otomatis adalah ke dalam tiga kategori berikut:
- Pengujian unit—Ini adalah pengujian yang sangat terfokus yang berjalan pada satu class, biasanya satu metode dalam class tersebut. Jika pengujian unit gagal, Anda dapat mengetahui dengan tepat di mana masalahnya dalam kode Anda. Pengujian ini memiliki fidelitas rendah karena di dunia nyata, aplikasi Anda melibatkan lebih dari sekadar eksekusi satu metode atau class. Alat ini cukup cepat untuk dijalankan setiap kali Anda mengubah kode. Pengujian ini biasanya dijalankan secara lokal (dalam set sumber
test
). Contoh: Menguji metode tunggal dalam model tampilan dan repositori. - Pengujian integrasi—Pengujian ini menguji interaksi beberapa class untuk memastikan class tersebut berperilaku seperti yang diharapkan saat digunakan bersama. Salah satu cara untuk menyusun pengujian integrasi adalah dengan menguji satu fitur, seperti kemampuan untuk menyimpan tugas. Pengujian ini menguji cakupan kode yang lebih besar daripada pengujian unit, tetapi tetap dioptimalkan untuk berjalan cepat, dibandingkan dengan memiliki fidelitas penuh. Pengujian ini dapat dijalankan secara lokal atau sebagai pengujian instrumentasi, bergantung pada situasinya. Contoh: Menguji semua fungsi dari satu pasangan fragmen dan model tampilan.
- Pengujian menyeluruh (E2E)—Menguji kombinasi fitur yang bekerja bersama. Pengujian ini menguji sebagian besar aplikasi, mensimulasikan penggunaan nyata secara cermat, dan oleh karena itu biasanya lambat. Pengujian ini memiliki fidelitas tertinggi dan memberi tahu Anda bahwa aplikasi Anda benar-benar berfungsi secara keseluruhan. Secara umum, pengujian ini akan menjadi pengujian berinstrumen (dalam set sumber
androidTest
)
Contoh: Memulai seluruh aplikasi dan menguji beberapa fitur secara bersamaan.
Proporsi yang disarankan untuk pengujian ini sering kali diwakili oleh piramida, dengan sebagian besar pengujian adalah pengujian unit.
Arsitektur dan Pengujian
Kemampuan Anda untuk menguji aplikasi di semua tingkat piramida pengujian secara inheren terkait dengan arsitektur aplikasi. Misalnya, aplikasi yang arsitekturnya sangat buruk mungkin menempatkan semua logikanya di dalam satu metode. Anda mungkin dapat menulis pengujian end-to-end untuk hal ini, karena pengujian ini cenderung menguji sebagian besar aplikasi, tetapi bagaimana dengan menulis pengujian unit atau integrasi? Dengan semua kode di satu tempat, sulit untuk menguji hanya kode yang terkait dengan satu unit atau fitur.
Pendekatan yang lebih baik adalah memecah logika aplikasi menjadi beberapa metode dan class, sehingga setiap bagian dapat diuji secara terpisah. Arsitektur adalah cara untuk membagi dan mengatur kode Anda, yang memungkinkan pengujian unit dan integrasi yang lebih mudah. Aplikasi TO-DO yang akan Anda uji mengikuti arsitektur tertentu:
Dalam pelajaran ini, Anda akan melihat cara menguji bagian-bagian arsitektur di atas, dalam isolasi yang tepat:
- Pertama, Anda akan melakukan pengujian unit pada repositori.
- Kemudian, Anda akan menggunakan test double di model tampilan, yang diperlukan untuk pengujian unit dan pengujian integrasi model tampilan.
- Selanjutnya, Anda akan mempelajari cara menulis pengujian integrasi untuk fragmen dan model tampilannya.
- Terakhir, Anda akan mempelajari cara menulis pengujian integrasi yang mencakup komponen Navigation.
Pengujian end-to-end akan dibahas di materi berikutnya.
Saat Anda menulis pengujian unit untuk bagian class (metode atau kumpulan kecil metode), tujuan Anda adalah hanya menguji kode di class tersebut.
Menguji hanya kode dalam satu atau beberapa class tertentu bisa jadi rumit. Mari perhatikan contoh berikut. Buka class data.source.DefaultTaskRepository
di set sumber main
. Ini adalah repositori untuk aplikasi, dan merupakan class yang akan Anda tulis pengujian unitnya berikutnya.
Tujuan Anda adalah menguji hanya kode di class tersebut. Namun, DefaultTaskRepository
bergantung pada class lain, seperti LocalTaskDataSource
dan RemoteTaskDataSource
, agar dapat berfungsi. Cara lain untuk mengatakan hal ini adalah bahwa LocalTaskDataSource
dan RemoteTaskDataSource
adalah dependensi dari DefaultTaskRepository
.
Jadi, setiap metode di DefaultTaskRepository
memanggil metode di class sumber data, yang pada gilirannya memanggil metode di class lain untuk menyimpan informasi ke database atau berkomunikasi dengan jaringan.
Misalnya, lihat metode ini di DefaultTasksRepo
.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}
getTasks
adalah salah satu panggilan "dasar" yang mungkin Anda lakukan ke repositori. Metode ini mencakup pembacaan dari database SQLite dan melakukan panggilan jaringan (panggilan ke updateTasksFromRemoteDataSource
). Hal ini melibatkan lebih banyak kode daripada hanya kode repositori.
Berikut adalah beberapa alasan yang lebih spesifik mengapa pengujian repositori sulit dilakukan:
- Anda harus berurusan dengan pemikiran tentang pembuatan dan pengelolaan database untuk melakukan pengujian paling sederhana untuk repositori ini. Hal ini menimbulkan pertanyaan seperti "apakah ini harus berupa pengujian lokal atau berinstrumen?" dan apakah Anda harus menggunakan AndroidX Test untuk mendapatkan lingkungan Android simulasi.
- Beberapa bagian kode, seperti kode jaringan, dapat memerlukan waktu lama untuk dijalankan, atau bahkan terkadang gagal, sehingga membuat pengujian yang berjalan lama dan tidak stabil.
- Pengujian Anda dapat kehilangan kemampuannya untuk mendiagnosis kode mana yang menyebabkan kegagalan pengujian. Pengujian Anda dapat mulai menguji kode non-repositori, jadi, misalnya, pengujian unit "repositori" Anda dapat gagal karena masalah pada beberapa kode dependen, seperti kode database.
Pengganti Pengujian
Solusinya adalah saat Anda menguji repositori, jangan gunakan kode jaringan atau database yang sebenarnya, tetapi gunakan test double. Pengujian ganda adalah versi class yang dibuat khusus untuk pengujian. Tujuannya adalah untuk menggantikan versi sebenarnya dari class dalam pengujian. Mirip dengan pemeran pengganti yang merupakan aktor yang berspesialisasi dalam adegan berbahaya, dan menggantikan aktor asli untuk adegan berbahaya.
Berikut beberapa jenis pengganti pengujian:
Palsu | Pengganti pengujian yang memiliki implementasi "berfungsi" dari class, tetapi diimplementasikan dengan cara yang membuatnya bagus untuk pengujian, tetapi tidak cocok untuk produksi. |
Mock | Pengganti pengujian yang melacak metode mana yang dipanggil. Kemudian, pengujian akan lulus atau gagal, bergantung pada apakah metodenya dipanggil dengan benar atau tidak. |
Stub | Pengganti pengujian yang tidak menyertakan logika dan hanya menampilkan apa yang Anda program untuk ditampilkan. |
Dummy | Pengganti pengujian yang diteruskan tetapi tidak digunakan, seperti jika Anda hanya perlu memberikannya sebagai parameter. Jika Anda memiliki |
Spy | Pengujian ganda yang juga melacak beberapa informasi tambahan; misalnya, jika Anda membuat |
Untuk mengetahui informasi selengkapnya tentang pengganda pengujian, lihat Testing on the Toilet: Know Your Test Doubles.
Dummy pengujian yang paling umum digunakan di Android adalah Palsu (Fake) dan Mock.
Dalam tugas ini, Anda akan membuat pengganti pengujian FakeDataSource
untuk menguji unit DefaultTasksRepository
yang terpisah dari sumber data sebenarnya.
Langkah 1: Buat class FakeDataSource
Pada langkah ini, Anda akan membuat class bernama FakeDataSouce
, yang akan menjadi test double dari LocalDataSource
dan RemoteDataSource
.
- Di set sumber test, klik kanan, lalu pilih New -> Package.
- Buat paket data dengan paket sumber di dalamnya.
- Buat class baru bernama
FakeDataSource
dalam paket data/source.
Langkah 2: Terapkan Antarmuka TasksDataSource
Agar dapat menggunakan class FakeDataSource
baru sebagai pengganti pengujian, class tersebut harus dapat menggantikan sumber data lainnya. Sumber data tersebut adalah TasksLocalDataSource
dan TasksRemoteDataSource
.
- Perhatikan bagaimana keduanya menerapkan antarmuka
TasksDataSource
.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- Buat
FakeDataSource
mengimplementasikanTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio akan mengeluh bahwa Anda belum menerapkan metode yang diperlukan untuk TasksDataSource
.
- Gunakan menu perbaikan cepat, lalu pilih Implement members.
- Pilih semua metode, lalu tekan Oke.
Langkah 3: Terapkan metode getTasks di FakeDataSource
FakeDataSource
adalah jenis pengganti pengujian tertentu yang disebut tiruan. Palsu adalah pengganti pengujian yang memiliki penerapan "berfungsi" dari class, tetapi diterapkan dengan cara yang membuatnya bagus untuk pengujian, tetapi tidak cocok untuk produksi. Implementasi "berfungsi" berarti class akan menghasilkan output yang realistis berdasarkan input yang diberikan.
Misalnya, sumber data palsu Anda tidak akan terhubung ke jaringan atau menyimpan apa pun ke database—melainkan hanya akan menggunakan daftar dalam memori. Hal ini akan "berfungsi seperti yang Anda harapkan" karena metode untuk mendapatkan atau menyimpan tugas akan menampilkan hasil yang diharapkan, tetapi Anda tidak akan pernah dapat menggunakan penerapan ini dalam produksi, karena tidak disimpan ke server atau database.
FakeDataSource
- memungkinkan Anda menguji kode di
DefaultTasksRepository
tanpa perlu mengandalkan database atau jaringan yang sebenarnya. - menyediakan implementasi yang "cukup nyata" untuk pengujian.
- Ubah konstruktor
FakeDataSource
untuk membuatvar
bernamatasks
yang merupakanMutableList<Task>?
dengan nilai default berupa daftar mutable kosong.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Ini adalah daftar tugas yang "meniru" respons database atau server. Untuk saat ini, tujuannya adalah menguji metode repositori getTasks
. Tindakan ini memanggil metode getTasks
, deleteAllTasks
, dan saveTask
sumber data .
Tulis versi palsu dari metode ini:
- Tulis
getTasks
: Jikatasks
bukannull
, tampilkan hasilSuccess
. Jikatasks
adalahnull
, tampilkan hasilError
. - Tulis
deleteAllTasks
: hapus daftar tugas yang dapat berubah. - Tulis
saveTask
: tambahkan tugas ke daftar.
Metode tersebut, yang diterapkan untuk FakeDataSource
, akan terlihat seperti kode di bawah ini.
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}
Berikut adalah pernyataan impor jika diperlukan:
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
Hal ini mirip dengan cara kerja sumber data lokal dan jarak jauh yang sebenarnya.
Pada langkah ini, Anda akan menggunakan teknik yang disebut injeksi dependensi manual sehingga Anda dapat menggunakan test double palsu yang baru saja Anda buat.
Masalah utamanya adalah Anda memiliki FakeDataSource
, tetapi tidak jelas bagaimana Anda menggunakannya dalam pengujian. Kode ini perlu menggantikan TasksRemoteDataSource
dan TasksLocalDataSource
, tetapi hanya dalam pengujian. TasksRemoteDataSource
dan TasksLocalDataSource
adalah dependensi DefaultTasksRepository
, yang berarti DefaultTasksRepositories
memerlukan atau "bergantung" pada class ini untuk dijalankan.
Saat ini, dependensi dibuat di dalam metode init
dari DefaultTasksRepository
.
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}
Karena Anda membuat dan menetapkan taskLocalDataSource
dan tasksRemoteDataSource
di dalam DefaultTasksRepository
, keduanya pada dasarnya dikodekan secara permanen. Tidak ada cara untuk mengganti pengujian ganda Anda.
Sebagai gantinya, Anda ingin menyediakan sumber data ini ke class, bukan meng-hard code-nya. Penyediaan dependensi dikenal sebagai injeksi dependensi. Ada berbagai cara untuk menyediakan dependensi, dan oleh karena itu ada berbagai jenis injeksi dependensi.
Dengan Constructor Dependency Injection, Anda dapat menukar test double dengan meneruskannya ke konstruktor.
Tidak ada injeksi | Injeksi |
Langkah 1: Gunakan Injeksi Dependensi Konstruktor di DefaultTasksRepository
- Ubah konstruktor
DefaultTaskRepository
dari yang menerimaApplication
menjadi yang menerima kedua sumber data dan dispatcher coroutine (yang juga perlu Anda tukar untuk pengujian - hal ini dijelaskan lebih mendetail di bagian pelajaran ketiga tentang coroutine).
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
- Karena Anda telah meneruskan dependensi, hapus metode
init
. Anda tidak perlu lagi membuat dependensi. - Hapus juga variabel instance lama. Anda menentukannya di konstruktor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- Terakhir, perbarui metode
getRepository
untuk menggunakan konstruktor baru:
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
Anda kini menggunakan injeksi dependensi konstruktor.
Langkah 2: Gunakan FakeDataSource dalam pengujian Anda
Setelah kode Anda menggunakan injeksi dependensi konstruktor, Anda dapat menggunakan sumber data palsu untuk menguji DefaultTasksRepository
.
- Klik kanan nama class
DefaultTasksRepository
, pilih Generate, lalu Test. - Ikuti petunjuk untuk membuat
DefaultTasksRepositoryTest
di set sumber test. - Di bagian atas class
DefaultTasksRepositoryTest
baru, tambahkan variabel anggota di bawah untuk merepresentasikan data di sumber data palsu Anda.
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
- Buat tiga variabel, dua variabel anggota
FakeDataSource
(satu untuk setiap sumber data untuk repositori Anda) dan variabel untukDefaultTasksRepository
yang akan Anda uji.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Buat metode untuk menyiapkan dan menginisialisasi DefaultTasksRepository
yang dapat diuji. DefaultTasksRepository
ini akan menggunakan test double Anda, FakeDataSource
.
- Buat metode dengan nama
createRepository
dan anotasikan dengan@Before
. - Buat instance sumber data palsu Anda, menggunakan daftar
remoteTasks
danlocalTasks
. - Buat instance
tasksRepository
, menggunakan dua sumber data palsu yang baru saja Anda buat danDispatchers.Unconfined
.
Metode akhir akan terlihat seperti kode di bawah.
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
Langkah 3: Tulis Pengujian DefaultTasksRepository getTasks()
Saatnya menulis pengujian DefaultTasksRepository
.
- Tulis pengujian untuk metode
getTasks
repositori. Periksa apakah saat Anda memanggilgetTasks
dengantrue
(artinya, data harus dimuat ulang dari sumber data jarak jauh), data yang ditampilkan berasal dari sumber data jarak jauh (bukan sumber data lokal).
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
Anda akan mendapatkan error saat memanggil getTasks:
Langkah 4: Tambahkan runBlockingTest
Error coroutine diharapkan terjadi karena getTasks
adalah fungsi suspend
dan Anda perlu meluncurkan coroutine untuk memanggilnya. Untuk itu, Anda memerlukan cakupan coroutine. Untuk mengatasi error ini, Anda harus menambahkan beberapa dependensi gradle untuk menangani peluncuran coroutine dalam pengujian.
- Tambahkan dependensi yang diperlukan untuk menguji coroutine ke set sumber pengujian menggunakan
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Jangan lupa sinkronkan!
kotlinx-coroutines-test
adalah library pengujian coroutine, yang secara khusus ditujukan untuk menguji coroutine. Untuk menjalankan pengujian, gunakan fungsi runBlockingTest
. Ini adalah fungsi yang disediakan oleh library pengujian coroutine. Fungsi ini menerima blok kode, lalu menjalankan blok kode ini dalam konteks coroutine khusus yang berjalan secara serentak dan langsung, yang berarti tindakan akan terjadi dalam urutan yang deterministik. Pada dasarnya, hal ini membuat coroutine Anda berjalan seperti non-coroutine, sehingga ditujukan untuk menguji kode.
Gunakan runBlockingTest
di class pengujian saat Anda memanggil fungsi suspend
. Anda akan mempelajari lebih lanjut cara kerja runBlockingTest
dan cara menguji coroutine di codelab berikutnya dalam seri ini.
- Tambahkan
@ExperimentalCoroutinesApi
di atas class. Hal ini menunjukkan bahwa Anda tahu Anda menggunakan API coroutine eksperimental (runBlockingTest
) di class. Tanpa itu, Anda akan mendapatkan peringatan. - Kembali di
DefaultTasksRepositoryTest
, tambahkanrunBlockingTest
sehingga dapat mengambil seluruh pengujian Anda sebagai "blok" kode
Pengujian akhir ini akan terlihat seperti kode di bawah.
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}
- Jalankan pengujian
getTasks_requestsAllTasksFromRemoteDataSource
baru Anda dan konfirmasi bahwa pengujian berfungsi dan errornya sudah hilang.
Anda baru saja melihat cara melakukan pengujian unit pada repositori. Pada langkah berikutnya, Anda akan menggunakan injeksi dependensi lagi dan membuat test double lain—kali ini untuk menunjukkan cara menulis pengujian unit dan integrasi untuk model tampilan.
Pengujian unit hanya boleh menguji class atau metode yang Anda minati. Hal ini dikenal sebagai pengujian secara terpisah, di mana Anda dengan jelas memisahkan "unit" dan hanya menguji kode yang merupakan bagian dari unit tersebut.
Jadi, TasksViewModelTest
hanya boleh menguji kode TasksViewModel
—tidak boleh menguji di database, jaringan, atau class repositori. Oleh karena itu, untuk model tampilan, seperti yang baru saja Anda lakukan untuk repositori, Anda akan membuat repositori palsu dan menerapkan injeksi dependensi untuk menggunakannya dalam pengujian.
Dalam tugas ini, Anda akan menerapkan injeksi dependensi ke model tampilan.
Langkah 1. Membuat Antarmuka TasksRepository
Langkah pertama untuk menggunakan injeksi dependensi konstruktor adalah membuat antarmuka umum yang digunakan bersama antara class palsu dan class asli.
Bagaimana praktiknya? Lihat TasksRemoteDataSource
, TasksLocalDataSource
, dan FakeDataSource
, lalu perhatikan bahwa semuanya memiliki antarmuka yang sama: TasksDataSource
. Hal ini memungkinkan Anda menyatakan di konstruktor DefaultTasksRepository
bahwa Anda mengambil TasksDataSource
.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
Hal ini memungkinkan kami memasukkan FakeDataSource
Anda.
Selanjutnya, buat antarmuka untuk DefaultTasksRepository
, seperti yang Anda lakukan untuk sumber data. Class ini harus menyertakan semua metode publik (platform API publik) DefaultTasksRepository
.
- Buka
DefaultTasksRepository
dan klik kanan nama class. Kemudian, pilih Refactor -> Extract -> Interface.
- Pilih Ekstrak ke file terpisah.
- Di jendela Extract Interface, ubah nama antarmuka menjadi
TasksRepository
. - Di bagian Members to form interface, centang semua anggota kecuali dua anggota pendamping dan metode pribadi.
- Klik Refactor. Antarmuka
TasksRepository
baru akan muncul di paket data/source .
Selain itu, DefaultTasksRepository
sekarang mengimplementasikan TasksRepository
.
- Jalankan aplikasi Anda (bukan pengujian) untuk memastikan semuanya masih berfungsi dengan baik.
Langkah 2. Membuat FakeTestRepository
Setelah memiliki antarmuka, Anda dapat membuat pengganti pengujian DefaultTaskRepository
.
- Di set sumber test, di data/source, buat file dan class Kotlin
FakeTestRepository.kt
dan perluas dari antarmukaTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Anda akan diberi tahu bahwa Anda perlu menerapkan metode antarmuka.
- Arahkan kursor ke error hingga Anda melihat menu saran, lalu klik dan pilih Implement members.
- Pilih semua metode, lalu tekan Oke.
Langkah 3. Mengimplementasikan metode FakeTestRepository
Sekarang Anda memiliki class FakeTestRepository
dengan metode "not implemented". Mirip dengan cara Anda menerapkan FakeDataSource
, FakeTestRepository
akan didukung oleh struktur data, bukan menangani mediasi yang rumit antara sumber data lokal dan jarak jauh.
Perhatikan bahwa FakeTestRepository
Anda tidak perlu menggunakan FakeDataSource
atau hal serupa; FakeTestRepository
hanya perlu menampilkan output palsu yang realistis berdasarkan input yang diberikan. Anda akan menggunakan LinkedHashMap
untuk menyimpan daftar tugas dan MutableLiveData
untuk tugas yang dapat diamati.
- Di
FakeTestRepository
, tambahkan variabelLinkedHashMap
yang merepresentasikan daftar tugas saat ini danMutableLiveData
untuk tugas yang dapat diamati.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
Terapkan metode berikut:
getTasks
—Metode ini harus mengambiltasksServiceData
dan mengubahnya menjadi daftar menggunakantasksServiceData.values.toList()
, lalu menampilkannya sebagai hasilSuccess
.refreshTasks
—Memperbarui nilaiobservableTasks
menjadi nilai yang ditampilkan olehgetTasks()
.observeTasks
—Membuat coroutine menggunakanrunBlocking
dan menjalankanrefreshTasks
, lalu menampilkanobservableTasks
.
Berikut adalah kode untuk metode tersebut.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}
Langkah 4. Menambahkan metode untuk pengujian ke addTasks
Saat menguji, sebaiknya sudah ada beberapa Tasks
di repositori Anda. Anda dapat memanggil saveTask
beberapa kali, tetapi untuk mempermudahnya, tambahkan metode bantuan khusus untuk pengujian yang memungkinkan Anda menambahkan tugas.
- Tambahkan metode
addTasks
, yang mengambilvararg
tugas, menambahkan setiap tugas keHashMap
, lalu memuat ulang tugas.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
Pada tahap ini, Anda memiliki repositori palsu untuk pengujian dengan beberapa metode utama yang diterapkan. Selanjutnya, gunakan ini dalam pengujian Anda.
Dalam tugas ini, Anda akan menggunakan class palsu di dalam ViewModel
. Gunakan injeksi dependensi konstruktor, untuk mengambil dua sumber data melalui injeksi dependensi konstruktor dengan menambahkan variabel TasksRepository
ke konstruktor TasksViewModel
.
Proses ini sedikit berbeda dengan model tampilan karena Anda tidak membuatnya secara langsung. Misalnya:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Seperti dalam kode di atas, Anda menggunakan delegasi properti viewModel's
yang membuat model tampilan. Untuk mengubah cara pembuatan model tampilan, Anda harus menambahkan dan menggunakan ViewModelProvider.Factory
. Jika Anda belum memahami ViewModelProvider.Factory
, Anda dapat mempelajari lebih lanjut di sini.
Langkah 1. Membuat dan menggunakan ViewModelFactory di TasksViewModel
Anda mulai dengan memperbarui class dan pengujian yang terkait dengan layar Tasks
.
- Buka
TasksViewModel
. - Ubah konstruktor
TasksViewModel
untuk menggunakanTasksRepository
, bukan membuatnya di dalam class.
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}
Karena Anda mengubah konstruktor, Anda sekarang harus menggunakan factory untuk membuat TasksViewModel
. Tempatkan class factory di file yang sama dengan TasksViewModel
, tetapi Anda juga dapat menempatkannya di file sendiri.
- Di bagian bawah file
TasksViewModel
, di luar class, tambahkanTasksViewModelFactory
yang mengambilTasksRepository
biasa.
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
Ini adalah cara standar untuk mengubah cara pembuatan ViewModel
. Setelah memiliki factory, gunakan factory tersebut di mana pun Anda membuat model tampilan.
- Update
TasksFragment
untuk menggunakan pabrik.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Jalankan kode aplikasi Anda dan pastikan semuanya masih berfungsi.
Langkah 2. Menggunakan FakeTestRepository di dalam TasksViewModelTest
Sekarang, alih-alih menggunakan repositori sebenarnya dalam pengujian model tampilan, Anda dapat menggunakan repositori palsu.
- Buka
TasksViewModelTest
. - Tambahkan properti
FakeTestRepository
diTasksViewModelTest
.
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}
- Perbarui metode
setupViewModel
untuk membuatFakeTestRepository
dengan tiga tugas, lalu buattasksViewModel
dengan repositori ini.
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}
- Karena Anda tidak lagi menggunakan kode AndroidX Test
ApplicationProvider.getApplicationContext
, Anda juga dapat menghapus anotasi@RunWith(AndroidJUnit4::class)
. - Jalankan pengujian Anda, pastikan semuanya masih berfungsi.
Dengan menggunakan injeksi dependensi konstruktor, Anda kini telah menghapus DefaultTasksRepository
sebagai dependensi dan menggantinya dengan FakeTestRepository
di pengujian.
Langkah 3. Juga Perbarui Fragmen dan ViewModel TaskDetail
Buat perubahan yang sama persis untuk TaskDetailFragment
dan TaskDetailViewModel
. Tindakan ini akan menyiapkan kode saat Anda menulis pengujian TaskDetail
berikutnya.
- Buka
TaskDetailViewModel
. - Perbarui konstruktor:
TaskDetailViewModel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
- Di bagian bawah file
TaskDetailViewModel
, di luar class, tambahkanTaskDetailViewModelFactory
.
TaskDetailViewModel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}
- Update
TasksFragment
untuk menggunakan pabrik.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Jalankan kode Anda dan pastikan semuanya berfungsi.
Sekarang Anda dapat menggunakan FakeTestRepository
, bukan repositori sebenarnya di TasksFragment
dan TasksDetailFragment
.
Selanjutnya, Anda akan menulis pengujian integrasi untuk menguji interaksi fragmen dan model tampilan. Anda akan mengetahui apakah kode model tampilan memperbarui UI dengan tepat. Untuk melakukannya, Anda menggunakan
- pola ServiceLocator
- library Espresso dan Mockito
Pengujian integrasi menguji interaksi beberapa class untuk memastikan class tersebut berperilaku seperti yang diharapkan saat digunakan bersama. Pengujian ini dapat dijalankan secara lokal (kumpulan sumber test
) atau sebagai pengujian instrumentasi (kumpulan sumber androidTest
).
Dalam kasus ini, Anda akan mengambil setiap fragmen dan menulis pengujian integrasi untuk fragmen dan model tampilan guna menguji fitur utama fragmen.
Langkah 1. Menambahkan Dependensi Gradle
- Tambahkan dependensi gradle berikut.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Dependensi ini mencakup:
junit:junit
—JUnit, yang diperlukan untuk menulis pernyataan pengujian dasar.androidx.test:core
—Library pengujian AndroidX intikotlinx-coroutines-test
—Library pengujian coroutineandroidx.fragment:fragment-testing
—Library pengujian AndroidX untuk membuat fragmen dalam pengujian dan mengubah statusnya.
Karena Anda akan menggunakan library ini di set sumber androidTest
, gunakan androidTestImplementation
untuk menambahkannya sebagai dependensi.
Langkah 2. Membuat class TaskDetailFragmentTest
TaskDetailFragment
menampilkan informasi tentang satu tugas.
Anda akan mulai dengan menulis pengujian fragmen untuk TaskDetailFragment
karena memiliki fungsi yang cukup mendasar dibandingkan dengan fragmen lainnya.
- Buka
taskdetail.TaskDetailFragment
. - Buat pengujian untuk
TaskDetailFragment
, seperti yang telah Anda lakukan sebelumnya. Terima pilihan default dan masukkan ke set sumber androidTest (BUKAN set sumbertest
).
- Tambahkan anotasi berikut ke class
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
Tujuan anotasi ini adalah:
@MediumTest
—Menandai pengujian sebagai pengujian integrasi "waktu proses sedang" (berbeda dengan pengujian unit@SmallTest
dan pengujian end-to-end besar@LargeTest
). Hal ini membantu Anda mengelompokkan dan memilih ukuran pengujian yang akan dijalankan.@RunWith(AndroidJUnit4::class)
—Digunakan di class mana pun yang menggunakan AndroidX Test.
Langkah 3. Meluncurkan fragmen dari pengujian
Dalam tugas ini, Anda akan meluncurkan TaskDetailFragment
menggunakan library Pengujian AndroidX. FragmentScenario
adalah class dari AndroidX Test yang membungkus fragmen dan memberi Anda kontrol langsung atas siklus proses fragmen untuk pengujian. Untuk menulis pengujian untuk fragmen, Anda membuat FragmentScenario
untuk fragmen yang Anda uji (TaskDetailFragment
).
- Salin pengujian ini ke
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Kode di atas:
- Membuat tugas.
- Membuat
Bundle
, yang merepresentasikan argumen fragmen untuk tugas yang diteruskan ke fragmen). - Fungsi
launchFragmentInContainer
membuatFragmentScenario
, dengan paket dan tema ini.
Ini belum merupakan pengujian yang selesai, karena tidak menegaskan apa pun. Untuk saat ini, jalankan pengujian dan amati apa yang terjadi.
- Ini adalah pengujian berinstrumen, jadi pastikan emulator atau perangkat Anda terlihat.
- Jalankan pengujian.
Beberapa hal akan terjadi.
- Pertama, karena ini adalah pengujian berinstrumen, pengujian akan berjalan di perangkat fisik Anda (jika terhubung) atau emulator.
- Fragment akan diluncurkan.
- Perhatikan bagaimana fragmen ini tidak menavigasi fragmen lain atau memiliki menu yang terkait dengan aktivitas - fragmen ini hanya fragmen.
Terakhir, perhatikan baik-baik dan perhatikan bahwa fragmen menampilkan "Tidak ada data" karena tidak berhasil memuat data tugas.
Pengujian Anda harus memuat TaskDetailFragment
(yang telah Anda lakukan) dan menegaskan bahwa data dimuat dengan benar. Mengapa tidak ada data? Hal ini karena Anda membuat tugas, tetapi tidak menyimpannya ke repositori.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Anda memiliki FakeTestRepository
ini, tetapi Anda memerlukan cara untuk mengganti repositori asli dengan repositori palsu untuk fragmen. Anda akan melakukannya nanti.
Dalam tugas ini, Anda akan menyediakan repositori palsu ke fragmen menggunakan ServiceLocator
. Dengan demikian, Anda dapat menulis pengujian integrasi model tampilan dan fragmen.
Anda tidak dapat menggunakan injeksi dependensi konstruktor di sini, seperti yang Anda lakukan sebelumnya, saat Anda perlu menyediakan dependensi ke model tampilan atau repositori. Injeksi dependensi konstruktor mengharuskan Anda membangun class. Fragmen dan aktivitas adalah contoh class yang tidak Anda buat dan umumnya tidak memiliki akses ke konstruktornya.
Karena Anda tidak membuat fragmen, Anda tidak dapat menggunakan injeksi dependensi konstruktor untuk menukar test double repositori (FakeTestRepository
) ke fragmen. Sebagai gantinya, gunakan pola Service Locator. Pola Pencari Layanan adalah alternatif untuk Injeksi Dependensi. Hal ini melibatkan pembuatan class singleton yang disebut "Service Locator", yang tujuannya adalah untuk menyediakan dependensi, baik untuk kode reguler maupun kode pengujian. Dalam kode aplikasi biasa (set sumber main
), semua dependensi ini adalah dependensi aplikasi biasa. Untuk pengujian, Anda mengubah Pencari Layanan untuk menyediakan versi pengganti pengujian dependensi.
Tidak menggunakan Pencari Layanan | Menggunakan Pencari Lokasi Servis |
Untuk aplikasi codelab ini, lakukan hal berikut:
- Buat class Service Locator yang dapat membuat dan menyimpan repositori. Secara default, perintah ini membuat repositori "normal".
- Faktorkan ulang kode Anda sehingga saat Anda memerlukan repositori, gunakan Pencari Layanan.
- Di class pengujian, panggil metode di Pencari Layanan yang mengganti repositori "normal" dengan test double Anda.
Langkah 1. Buat ServiceLocator
Mari buat class ServiceLocator
. File ini akan berada di set sumber utama dengan kode aplikasi lainnya karena digunakan oleh kode aplikasi utama.
Catatan: ServiceLocator
adalah singleton, jadi gunakan kata kunci Kotlin object
untuk class.
- Buat file ServiceLocator.kt di tingkat teratas set sumber utama.
- Tentukan
object
bernamaServiceLocator
. - Buat variabel instance
database
danrepository
, lalu tetapkan keduanya kenull
. - Anotasikan repositori dengan
@Volatile
karena dapat digunakan oleh beberapa thread (@Volatile
dijelaskan secara mendetail di sini).
Kode Anda akan terlihat seperti yang ditunjukkan di bawah.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Saat ini, satu-satunya hal yang perlu dilakukan ServiceLocator
adalah mengetahui cara menampilkan TasksRepository
. Metode ini akan menampilkan DefaultTasksRepository
yang sudah ada atau membuat dan menampilkan DefaultTasksRepository
baru, jika diperlukan.
Tentukan fungsi berikut:
provideTasksRepository
—Menyediakan repositori yang sudah ada atau membuat yang baru. Metode ini harussynchronized
padathis
untuk menghindari, dalam situasi dengan beberapa thread yang berjalan, pembuatan dua instance repositori secara tidak sengaja.createTasksRepository
—Kode untuk membuat repositori baru. Akan memanggilcreateTaskLocalDataSource
dan membuatTasksRemoteDataSource
baru.createTaskLocalDataSource
—Kode untuk membuat sumber data lokal baru. Akan meneleponcreateDataBase
.createDataBase
—Kode untuk membuat database baru.
Kode yang sudah selesai ada di bawah.
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}
Langkah 2. Menggunakan ServiceLocator di Aplikasi
Anda akan membuat perubahan pada kode aplikasi utama (bukan pengujian) sehingga Anda membuat repositori di satu tempat, yaitu ServiceLocator
.
Penting agar Anda hanya membuat satu instance class repositori. Untuk memastikannya, Anda akan menggunakan Service locator di class Application saya.
- Di level teratas hierarki paket, buka
TodoApplication
dan buatval
untuk repositori Anda, lalu tetapkan repositori yang diperoleh menggunakanServiceLocator.provideTaskRepository
.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
Setelah membuat repositori di aplikasi, Anda dapat menghapus metode getRepository
lama di DefaultTasksRepository
.
- Buka
DefaultTasksRepository
dan hapus objek pendamping.
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
Sekarang, di mana pun Anda menggunakan getRepository
, gunakan taskRepository
aplikasi sebagai gantinya. Tindakan ini memastikan bahwa alih-alih membuat repositori secara langsung, Anda mendapatkan repositori apa pun yang disediakan ServiceLocator
.
- Buka
TaskDetailFragement
dan temukan panggilan kegetRepository
di bagian atas class. - Ganti panggilan ini dengan panggilan yang mendapatkan repositori dari
TodoApplication
.
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
- Lakukan hal yang sama untuk
TasksFragment
.
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
- Untuk
StatisticsViewModel
danAddEditTaskViewModel
, perbarui kode yang mendapatkan repositori untuk menggunakan repositori dariTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Jalankan aplikasi Anda (bukan pengujian).
Karena Anda hanya melakukan refaktorisasi, aplikasi akan berjalan sama tanpa masalah.
Langkah 3. Membuat FakeAndroidTestRepository
Anda sudah memiliki FakeTestRepository
di set sumber pengujian. Anda tidak dapat membagikan class pengujian antara set sumber test
dan androidTest
secara default. Jadi, Anda perlu membuat class FakeTestRepository
duplikat di set sumber androidTest
, dan memanggilnya FakeAndroidTestRepository
.
- Klik kanan set sumber
androidTest
dan buat paket data. Klik kanan lagi dan buat paket sumber . - Buat class baru dalam paket sumber ini bernama
FakeAndroidTestRepository.kt
. - Salin kode berikut ke class tersebut.
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
Langkah 4. Menyiapkan ServiceLocator untuk Pengujian
Oke, saatnya menggunakan ServiceLocator
untuk menukar dummy pengujian saat melakukan pengujian. Untuk melakukannya, Anda perlu menambahkan beberapa kode ke kode ServiceLocator
.
- Buka
ServiceLocator.kt
. - Tandai setter untuk
tasksRepository
sebagai@VisibleForTesting
. Anotasi ini adalah cara untuk menyatakan bahwa alasan setter bersifat publik adalah karena pengujian.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Baik Anda menjalankan pengujian sendiri atau dalam grup pengujian, pengujian Anda harus berjalan dengan cara yang sama persis. Artinya, pengujian Anda tidak boleh memiliki perilaku yang bergantung satu sama lain (yang berarti menghindari berbagi objek antar-pengujian).
Karena ServiceLocator
adalah singleton, ada kemungkinan ServiceLocator
dibagikan secara tidak sengaja di antara pengujian. Untuk membantu menghindari hal ini, buat metode yang mereset status ServiceLocator
dengan benar di antara pengujian.
- Tambahkan variabel instance bernama
lock
dengan nilaiAny
.
ServiceLocator.kt
private val lock = Any()
- Tambahkan metode khusus pengujian yang disebut
resetRepository
yang menghapus database dan menyetel repositori dan database ke null.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
Langkah 5. Menggunakan ServiceLocator Anda
Pada langkah ini, Anda akan menggunakan ServiceLocator
.
- Buka
TaskDetailFragmentTest
. - Deklarasikan variabel
lateinit TasksRepository
. - Tambahkan metode penyiapan dan penghentian untuk menyiapkan
FakeAndroidTestRepository
sebelum setiap pengujian dan membersihkannya setelah setiap pengujian.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Gabungkan isi fungsi
activeTaskDetails_DisplayedInUi()
dalamrunBlockingTest
. - Simpan
activeTask
di repositori sebelum meluncurkan fragmen.
repository.saveTask(activeTask)
Pengujian akhir akan terlihat seperti kode di bawah ini.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
- Anotasikan seluruh class dengan
@ExperimentalCoroutinesApi
.
Setelah selesai, kodenya akan terlihat seperti ini.
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- Jalankan pengujian
activeTaskDetails_DisplayedInUi()
.
Seperti sebelumnya, Anda akan melihat fragmen, tetapi kali ini, karena Anda telah menyiapkan repositori dengan benar, fragmen tersebut akan menampilkan informasi tugas.
Pada langkah ini, Anda akan menggunakan library pengujian UI Espresso untuk menyelesaikan pengujian integrasi pertama. Anda telah menyusun kode sehingga dapat menambahkan pengujian dengan pernyataan untuk UI. Untuk melakukannya, Anda akan menggunakan library pengujian Espresso.
Espresso membantu Anda:
- Berinteraksi dengan tampilan, seperti mengklik tombol, menggeser panel, atau men-scroll layar ke bawah.
- Menyatakan bahwa tampilan tertentu ada di layar atau dalam status tertentu (seperti berisi teks tertentu, atau bahwa kotak centang dicentang, dll.).
Langkah 1. Catatan Dependensi Gradle
Anda akan memiliki dependensi Espresso utama karena dependensi ini disertakan dalam project Android secara default.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
—Dependensi Espresso inti ini disertakan secara default saat Anda membuat project Android baru. File ini berisi kode pengujian dasar untuk sebagian besar tampilan dan tindakan di dalamnya.
Langkah 2. Menonaktifkan animasi
Pengujian Espresso berjalan di perangkat sungguhan dan dengan demikian merupakan pengujian instrumentasi. Salah satu masalah yang muncul adalah animasi: Jika animasi tertunda dan Anda mencoba menguji apakah tampilan ada di layar, tetapi masih menganimasikan, Espresso dapat secara tidak sengaja gagal dalam pengujian. Hal ini dapat membuat pengujian Espresso tidak stabil.
Untuk pengujian UI Espresso, sebaiknya nonaktifkan animasi (pengujian Anda juga akan berjalan lebih cepat):
- Di perangkat pengujian, buka Setelan > Opsi developer.
- Nonaktifkan tiga setelan ini: Skala animasi jendela, Skala animasi transisi, dan Skala durasi animator.
Langkah 3. Melihat pengujian Espresso
Sebelum menulis pengujian Espresso, lihat beberapa kode Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
Pernyataan ini akan menemukan tampilan kotak centang dengan ID task_detail_complete_checkbox
, mengkliknya, lalu menegaskan bahwa kotak centang tersebut dicentang.
Sebagian besar pernyataan Espresso terdiri dari empat bagian:
onView
onView
adalah contoh metode Espresso statis yang memulai pernyataan Espresso. onView
adalah salah satu yang paling umum, tetapi ada opsi lain, seperti onData
.
2. ViewMatcher
withId(R.id.task_detail_title_text)
withId
adalah contoh ViewMatcher
yang mendapatkan tampilan berdasarkan ID-nya. Ada pencocok tampilan lain yang dapat Anda cari di dokumentasi.
3. ViewAction
perform(click())
Metode perform
yang mengambil ViewAction
. ViewAction
adalah sesuatu yang dapat dilakukan pada tampilan, misalnya di sini, mengklik tampilan.
check(matches(isChecked()))
check
yang menggunakan ViewAssertion
. ViewAssertion
memeriksa atau menegaskan sesuatu tentang tampilan. ViewAssertion
yang paling umum digunakan adalah pernyataan matches
. Untuk menyelesaikan pernyataan, gunakan ViewMatcher
lain, dalam hal ini isChecked
.
Perhatikan bahwa Anda tidak selalu memanggil perform
dan check
dalam pernyataan Espresso. Anda dapat memiliki pernyataan yang hanya membuat pernyataan menggunakan check
atau hanya melakukan ViewAction
menggunakan perform
.
- Buka
TaskDetailFragmentTest.kt
. - Perbarui pengujian
activeTaskDetails_DisplayedInUi
.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
Berikut adalah pernyataan impor, jika diperlukan:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
- Semua yang ada setelah komentar
// THEN
menggunakan Espresso. Periksa struktur pengujian dan penggunaanwithId
serta periksa untuk membuat pernyataan tentang tampilan halaman detail. - Jalankan pengujian dan konfirmasi bahwa pengujian berhasil.
Langkah 4. Opsional, Menulis Pengujian Espresso Anda Sendiri
Sekarang tulis pengujian Anda sendiri.
- Buat pengujian baru bernama
completedTaskDetails_DisplayedInUi
, lalu salin kode kerangka ini.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}
- Melihat pengujian sebelumnya, selesaikan pengujian ini.
- Jalankan dan konfirmasi bahwa pengujian berhasil.
completedTaskDetails_DisplayedInUi
yang sudah selesai akan terlihat seperti kode ini.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}
Pada langkah terakhir ini, Anda akan mempelajari cara menguji komponen Navigation, menggunakan jenis pengganda pengujian yang berbeda yang disebut tiruan, dan library pengujian Mockito.
Dalam codelab ini, Anda telah menggunakan test double yang disebut tiruan. Fake adalah salah satu dari banyak jenis pengganda pengujian. Pengganti pengujian mana yang harus Anda gunakan untuk menguji komponen Navigasi?
Pikirkan cara navigasi terjadi. Bayangkan menekan salah satu tugas di TasksFragment
untuk membuka layar detail tugas.
Berikut adalah kode di TasksFragment
yang membuka layar detail tugas saat ditekan.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
Navigasi terjadi karena panggilan ke metode navigate
. Jika Anda perlu menulis pernyataan assert, tidak ada cara mudah untuk menguji apakah Anda telah membuka TaskDetailFragment
. Navigasi adalah tindakan rumit yang tidak menghasilkan output yang jelas atau perubahan status, di luar inisialisasi TaskDetailFragment
.
Yang dapat Anda tegaskan adalah bahwa metode navigate
dipanggil dengan parameter tindakan yang benar. Inilah yang dilakukan oleh pengganti pengujian mock—memeriksa apakah metode tertentu dipanggil.
Mockito adalah framework untuk membuat test double. Meskipun kata tiruan digunakan dalam API dan nama, kata ini bukan hanya untuk membuat tiruan. Mock juga dapat membuat stub dan spy.
Anda akan menggunakan Mockito untuk membuat tiruan NavigationController
yang dapat menyatakan bahwa metode navigasi dipanggil dengan benar.
Langkah 1. Menambahkan Dependensi Gradle
- Tambahkan dependensi gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core
—Ini adalah dependensi Mockito.dexmaker-mockito
—Library ini diperlukan untuk menggunakan Mockito dalam project Android. Mockito perlu membuat class saat runtime. Di Android, hal ini dilakukan menggunakan kode byte dex, sehingga library ini memungkinkan Mockito membuat objek selama runtime di Android.androidx.test.espresso:espresso-contrib
—Library ini terdiri dari kontribusi eksternal (oleh karena itu dinamakan demikian) yang berisi kode pengujian untuk tampilan yang lebih canggih, sepertiDatePicker
danRecyclerView
. File ini juga berisi pemeriksaan Aksesibilitas dan class bernamaCountingIdlingResource
yang akan dibahas nanti.
Langkah 2. Buat TasksFragmentTest
- Buka
TasksFragment
. - Klik kanan nama class
TasksFragment
, pilih Generate, lalu Test. Buat pengujian di set sumber androidTest. - Salin kode ini ke
TasksFragmentTest
.
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}
Kode ini terlihat mirip dengan kode TaskDetailFragmentTest
yang Anda tulis. Menyiapkan dan menghapus FakeAndroidTestRepository
. Tambahkan pengujian navigasi untuk menguji bahwa saat Anda mengklik tugas dalam daftar tugas, Anda akan diarahkan ke TaskDetailFragment
yang benar.
- Tambahkan pengujian
clickTask_navigateToDetailFragmentOne
.
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- Gunakan fungsi
mock
Mockito untuk membuat tiruan.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Untuk membuat tiruan di Mockito, teruskan kelas yang ingin Anda buat tiruannya.
Selanjutnya, Anda perlu mengaitkan NavController
dengan fragmen. onFragment
memungkinkan Anda memanggil metode pada fragmen itu sendiri.
- Jadikan tiruan baru Anda
NavController
fragmen.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Tambahkan kode untuk mengklik item di
RecyclerView
yang memiliki teks "TITLE1".
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
RecyclerViewActions
adalah bagian dari library espresso-contrib
dan memungkinkan Anda melakukan tindakan Espresso pada RecyclerView.
- Verifikasi bahwa
navigate
dipanggil, dengan argumen yang benar.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
Metode verify
Mockito-lah yang membuat ini menjadi tiruan—Anda dapat mengonfirmasi bahwa navController
tiruan memanggil metode tertentu (navigate
) dengan parameter (actionTasksFragmentToTaskDetailFragment
dengan ID "id1").
Pengujian lengkapnya akan terlihat seperti ini:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}
- Jalankan pengujian.
Singkatnya, untuk menguji navigasi, Anda dapat:
- Gunakan Mockito untuk membuat tiruan
NavController
. - Lampirkan
NavController
yang di-mock tersebut ke fragmen. - Pastikan navigate dipanggil dengan tindakan dan parameter yang benar.
Langkah 3. Opsional, tulis clickAddTaskButton_navigateToAddEditFragment
Untuk melihat apakah Anda dapat menulis sendiri pengujian navigasi, coba lakukan tugas ini.
- Tulis pengujian
clickAddTaskButton_navigateToAddEditFragment
yang memeriksa apakah jika Anda mengklik FAB +, Anda akan membukaAddEditTaskFragment
.
Jawabannya ada di bawah.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}
Klik di sini untuk melihat perbedaan antara kode yang Anda mulai dan kode akhir.
Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah git di bawah:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
Atau, Anda dapat mendownload repositori sebagai file ZIP, mengekstraknya, dan membukanya di Android Studio.
Codelab ini membahas cara menyiapkan injeksi dependensi manual, pencari layanan, dan cara menggunakan tiruan dan mock di aplikasi Android Kotlin Anda. Khususnya:
- Hal yang ingin Anda uji dan strategi pengujian Anda menentukan jenis pengujian yang akan Anda terapkan untuk aplikasi Anda. Pengujian unit berfokus dan cepat. Pengujian integrasi memverifikasi interaksi antarbagian program Anda. Pengujian end-to-end memverifikasi fitur, memiliki fidelitas tertinggi, sering kali diinstrumentasi, dan mungkin memerlukan waktu lebih lama untuk dijalankan.
- Arsitektur aplikasi Anda memengaruhi seberapa sulit pengujiannya.
- TDD atau Pengembangan Berdasarkan Pengujian adalah strategi di mana Anda menulis pengujian terlebih dahulu, lalu membuat fitur untuk lulus pengujian.
- Untuk mengisolasi bagian aplikasi Anda untuk pengujian, Anda dapat menggunakan pengujian ganda. Pengujian ganda adalah versi class yang dibuat khusus untuk pengujian. Misalnya, Anda memalsukan pengambilan data dari database atau internet.
- Gunakan injeksi dependensi untuk mengganti class asli dengan class pengujian, misalnya, repositori atau lapisan jaringan.
- Gunakan pengujian berinstrumen (
androidTest
) untuk meluncurkan komponen UI. - Jika Anda tidak dapat menggunakan injeksi dependensi konstruktor, misalnya untuk meluncurkan fragmen, Anda sering kali dapat menggunakan pencari layanan. Pola Pencari Lokasi Layanan adalah alternatif untuk Injeksi Dependensi. Hal ini melibatkan pembuatan class singleton yang disebut "Service Locator", yang tujuannya adalah untuk menyediakan dependensi, baik untuk kode reguler maupun kode pengujian.
Kursus Udacity:
Dokumentasi developer Android:
- Panduan untuk arsitektur aplikasi
runBlocking
danrunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- Set sumber
- Menguji dari command line
Video:
Lainnya:
Untuk mengetahui link ke codelab lain dalam kursus ini, lihat halaman landing codelab Android Lanjutan di Kotlin.