Codelab ini adalah bagian dari kursus Lanjutan Android di Kotlin. Anda akan mendapatkan manfaat maksimal dari kursus ini jika Anda mengerjakan codelab secara berurutan, tetapi ini tidak wajib. Semua codelab kursus tercantum di halaman landing codelab Android Lanjutan di Kotlin.
Pengantar
Codelab pengujian kedua ini membahas semua pengujian ganda: kapan menggunakannya di Android, dan cara mengimplementasikannya menggunakan injeksi dependensi, pola Pencari Lokasi Layanan, dan library. Dalam 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, Matcher, pengujian AndroidX, Robolectric, serta Menguji LiveData
- Library Android Jetpack inti berikut:
ViewModel
,LiveData
dan Komponen Navigasi - Arsitektur aplikasi, dengan 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 pengujian ganda, yaitu tiruan dan tiruan
- Cara menggunakan injeksi dependensi manual di Android untuk pengujian unit dan integrasi
- Cara menerapkan Pola Pencari Lokasi Layanan
- Cara menguji repositori, fragmen, model tampilan, dan komponen Navigasi
Anda akan menggunakan library dan konsep kode berikut:
Yang akan Anda lakukan
- Menulis pengujian unit untuk repositori menggunakan pengujian ganda dan injeksi dependensi.
- Menulis pengujian unit untuk model tampilan menggunakan pengujian ganda dan injeksi dependensi.
- Menulis pengujian integrasi untuk fragmen dan model tampilannya menggunakan framework pengujian UI Espresso.
- Menulis pengujian navigasi menggunakan Mockito dan Espresso.
Dalam serangkaian codelab ini, Anda akan mengerjakan aplikasi Catatan TO-DO. Aplikasi ini memungkinkan Anda menulis tugas untuk diselesaikan dan menampilkannya dalam daftar. Selanjutnya, Anda dapat menandainya sebagai selesai atau tidak, memfilter, 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 untuk membiasakan diri dengan kodenya, dengan mengikuti petunjuk di bawah.
Langkah 1: Jalankan contoh aplikasi
Setelah mendownload aplikasi Daftar Tugas, buka aplikasi di Android Studio dan jalankan. Kode harus dikompilasi. Jelajahi aplikasi dengan melakukan hal berikut:
- Buat tugas baru dengan tombol plus tindakan mengambang. 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 menetapkan statusnya menjadi Selesai.
- Kembali ke layar tugas, buka menu filter, dan filter tugas berdasarkan status Aktif dan Selesai.
- Buka panel navigasi dan 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 Daftar Tugas didasarkan pada contoh pengujian dan arsitektur Arsitektur Biru yang populer (menggunakan versi arsitektur reaktif dari contoh). Aplikasi mengikuti arsitektur dari Panduan arsitektur aplikasi. Menggunakan ViewModels dengan Fragment, repositori, dan Room. Jika Anda terbiasa dengan salah satu contoh di bawah, aplikasi ini memiliki arsitektur yang serupa:
- Room dengan Codelab View
- Codelab pelatihan Dasar-Dasar Kotlin Android
- Codelab pelatihan Android lanjutan
- Contoh Android Sunflower
- Kursus pelatihan Aplikasi Android dengan Kotlin Udacity
Anda harus memahami arsitektur umum aplikasi daripada memiliki pemahaman mendalam tentang logika di satu lapisan.
Berikut ringkasan paket yang akan Anda temukan:
Paket: | |
| Menambahkan atau mengedit layar tugas: Kode lapisan UI untuk menambahkan atau mengedit tugas. |
| Lapisan data: Hal ini berkaitan dengan lapisan data tugas. Project ini berisi database, jaringan, dan kode 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 geser ulang yang digunakan di beberapa layar. |
Lapisan data (.data)
Aplikasi ini menyertakan lapisan jaringan yang disimulasikan, dalam paket jarak jauh, dan lapisan database, dalam paket lokal. Untuk mempermudah, dalam project ini lapisan jaringan disimulasikan hanya dengan HashMap
dengan penundaan, daripada membuat permintaan jaringan yang sebenarnya.
Koordinat DefaultTasksRepository
atau memediasi antara lapisan jaringan dan lapisan database serta merupakan yang mengembalikan data ke lapisan UI.
Lapisan UI ( .addedittask, .statistics, .taskdetail, .tasks)
Setiap paket lapisan UI berisi fragmen dan model tampilan, bersama dengan class lain yang diperlukan untuk UI (seperti adaptor untuk daftar tugas). TaskActivity
adalah aktivitas yang berisi semua fragmen.
Navigasi
Navigasi untuk aplikasi dikontrol oleh Komponen navigasi. Hal ini ditentukan dalam file nav_graph.xml
. Navigasi dipicu dalam model tampilan menggunakan class Event
; model tampilan juga menentukan argumen yang akan diteruskan. Fragmen mengamati Event
dan melakukan navigasi sebenarnya di antara layar.
Dalam codelab ini, Anda akan mempelajari cara menguji repositori, melihat model, dan fragmen menggunakan pengujian ganda dan injeksi dependensi. Sebelum Anda mempelajari lebih dalam mengenai hal itu, penting untuk memahami alasan yang akan memandu apa dan bagaimana Anda akan menulis pengujian ini.
Bagian ini membahas beberapa praktik terbaik pengujian secara umum, yang berlaku untuk Android.
Piramida Pengujian
Saat memikirkan strategi pengujian, ada tiga aspek pengujian yang terkait:
- Cakupan—Berapa banyak kode yang disentuh pengujian? Pengujian dapat berjalan pada satu metode, di seluruh aplikasi, atau di antara keduanya.
- Kecepatan—Seberapa cepat pengujian berjalan? Kecepatan uji dapat bervariasi dari mili-detik hingga beberapa menit.
- Fidelitas—Seberapa "dunia nyata" adalah ujiannya? Misalnya, jika bagian dari kode yang diuji, Anda perlu membuat permintaan jaringan, apakah kode pengujian benar-benar membuat permintaan jaringan ini atau apakah hasilnya palsu? Jika pengujian benar-benar terhubung dengan jaringan, artinya jaringan ini memiliki fidelitas yang lebih tinggi. Konsekuensinya adalah pengujian dapat berjalan lebih lama, dapat menyebabkan error jika jaringan tidak aktif, atau dapat memerlukan banyak biaya.
Ada kompromi yang melekat di antara aspek-aspek tersebut. Misalnya, kecepatan dan fidelitas adalah kompromi—semakin cepat pengujian, umumnya, kurang fidelitas, dan sebaliknya. Satu cara umum untuk membagi pengujian otomatis adalah dengan tiga kategori ini:
- Pengujian unit—Ini adalah pengujian yang sangat terfokus yang berjalan pada satu kelas, biasanya satu metode di kelas tersebut. Jika pengujian unit gagal, Anda dapat mengetahui dengan tepat di mana kode Anda berada. Kode ini memiliki fidelitas yang rendah karena di dunia nyata, aplikasi Anda melibatkan jauh lebih banyak daripada eksekusi satu metode atau class. Fungsi ini cukup cepat untuk dijalankan setiap kali Anda mengubah kode. Pengujian paling sering dilakukan secara lokal (dalam set sumber
test
). Contoh: Menguji satu metode dalam model tampilan dan repositori. - Pengujian integrasi—Pengujian ini menguji interaksi dari beberapa class untuk memastikan perilakunya sesuai dengan yang diharapkan saat digunakan bersama. Salah satu cara untuk menyusun pengujian integrasi adalah dengan meminta mereka menguji satu fitur, seperti kemampuan untuk menyimpan tugas. Pengujian ini menguji cakupan kode yang lebih besar daripada pengujian unit, tetapi masih dioptimalkan untuk berjalan cepat dibandingkan dengan fidelitas sepenuhnya. Alat ini dapat dijalankan secara lokal atau sebagai uji instrumentasi, bergantung pada situasinya. Contoh: Menguji semua fungsi fragmen tunggal dan pasangan model tampilan.
- Pengujian menyeluruh (E2e)—Uji kombinasi fitur yang berfungsi bersama. Pengujian ini menguji sebagian besar aplikasi, menyimulasikan penggunaan nyata dengan cermat, dan biasanya lambat. Library ini memiliki fidelitas tertinggi dan memberi tahu Anda bahwa aplikasi Anda benar-benar berfungsi secara keseluruhan. Umumnya, pengujian ini akan diinstrumentasikan (dalam set sumber
androidTest
)
Contoh: Memulai seluruh aplikasi dan menguji beberapa fitur secara bersamaan.
Proporsi pengujian yang disarankan ini sering kali diwakili oleh piramida, dengan sebagian besar pengujian berupa pengujian unit.
Arsitektur dan Pengujian
Kemampuan Anda untuk menguji aplikasi di semua level yang berbeda dalam piramida pengujian pada dasarnya terkait dengan arsitektur aplikasi Anda. Misalnya, aplikasi yang berarsitektur sangat buruk dapat menempatkan semua logikanya dalam satu metode. Anda mungkin dapat menulis pengujian menyeluruh untuk pengujian ini, karena pengujian ini cenderung menguji sebagian besar aplikasi, tetapi bagaimana dengan pengujian unit atau pengujian integrasi? Dengan semua kode di satu tempat, sulit untuk menguji kode yang terkait dengan satu unit atau fitur saja.
Pendekatan yang lebih baik adalah memecah logika aplikasi menjadi beberapa metode dan class, sehingga memungkinkan setiap bagian diuji secara terpisah. Arsitektur adalah cara untuk membagi dan mengatur kode Anda, yang memungkinkan pengujian unit dan integrasi dengan lebih mudah. Aplikasi TO-DO yang akan Anda uji mengikuti arsitektur tertentu:
Dalam pelajaran ini, Anda akan melihat cara menguji bagian dari arsitektur di atas, secara terpisah:
- Pertama-tama, Anda akan menguji unit repositori.
- Kemudian, Anda akan menggunakan pengujian ganda dalam model tampilan, yang diperlukan untuk pengujian unit dan pengujian integrasi model tampilan.
- Berikutnya, Anda akan belajar menulis pengujian integrasi untuk fragmen dan model tampilannya.
- Terakhir, Anda akan mempelajari cara menulis pengujian integrasi yang menyertakan Komponen navigasi.
Pengujian menyeluruh akan dibahas dalam tutorial berikutnya.
Saat Anda menulis pengujian unit untuk bagian class (metode atau kumpulan kecil metode), tujuan Anda adalah hanya menguji kode dalam class tersebut.
Hanya menguji kode di satu atau beberapa kelas tertentu bisa jadi sulit. Perhatikan contoh berikut. Buka class data.source.DefaultTaskRepository
di set sumber main
. Ini adalah repositori untuk aplikasi, dan merupakan class tempat Anda akan menulis pengujian unit berikutnya.
Sasaran Anda adalah menguji kode hanya di kelas tersebut. Namun, DefaultTaskRepository
bergantung pada class lain, seperti LocalTaskDataSource
dan RemoteTaskDataSource
, untuk berfungsi. Cara lain untuk mengatakan ini adalah bahwa LocalTaskDataSource
dan RemoteTaskDataSource
adalah dependensi dari DefaultTaskRepository
.
Jadi, setiap metode di DefaultTaskRepository
memanggil metode di class sumber data, yang kemudian 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 buat ke repositori. Metode ini mencakup membaca dari database SQLite dan melakukan panggilan jaringan (panggilan ke updateTasksFromRemoteDataSource
). Hal ini melibatkan jauh lebih banyak kode daripada hanya kode repositori.
Berikut beberapa alasan yang lebih spesifik mengapa menguji repositori itu sulit:
- Anda perlu berurusan dengan berpikir untuk membuat dan mengelola database bahkan untuk melakukan pengujian paling sederhana bagi repositori ini. Ini akan menimbulkan pertanyaan seperti "apakah ini adalah pengujian lokal atau berinstrumen?" dan jika Anda harus menggunakan AndroidX Test untuk mendapatkan lingkungan Android simulasi.
- Beberapa bagian kode, seperti kode jaringan, dapat memerlukan waktu lama untuk dijalankan, atau terkadang bahkan gagal, sehingga membuat pengujian yang tidak stabil dan berjalan lama.
- Pengujian Anda dapat kehilangan kemampuannya untuk mendiagnosis kode yang salah atas kegagalan pengujian. Pengujian Anda bisa mulai menguji kode non-repositori, sehingga pengujian misalnya, seharusnya "repositori" unit bisa gagal karena ada masalah di beberapa kode dependen, seperti kode database.
Double Pengujian
Solusi untuk masalah ini adalah saat Anda menguji repositori, jangan gunakan kode database atau jaringan yang sebenarnya, tetapi gunakan pengujian ganda. Pengujian ganda adalah versi class yang dibuat khusus untuk pengujian. Hal ini dimaksudkan untuk mengganti versi asli class dalam pengujian. Ini mirip dengan bagaimana adegan aksi ganda adalah aktor yang ahli dalam aksi stunt, dan menggantikan aktor sungguhan untuk tindakan berbahaya.
Berikut adalah beberapa jenis pengujian ganda:
Palsu | Pengujian ganda yang memiliki penerapan "kerja" kelas, tetapi diterapkan dengan cara yang membuatnya cocok untuk pengujian tetapi tidak cocok untuk produksi. |
Tiruan | Pengujian ganda yang melacak metode mana yang dipanggil. Pengujian ini kemudian lulus atau gagal dalam pengujian bergantung pada apakah metodenya dipanggil dengan benar atau tidak. |
Stub | Pengujian ganda yang tidak menyertakan logika dan hanya menampilkan apa yang Anda programkan untuk ditampilkan. Misalnya, |
Dummy | Pengujian ganda yang diteruskan tetapi tidak digunakan, seperti jika Anda hanya perlu menyediakannya sebagai parameter. Jika Anda memiliki |
Mata-mata | Pengujian ganda yang juga melacak beberapa informasi tambahan; misalnya, jika Anda membuat |
Untuk informasi selengkapnya tentang pengujian ganda, lihat Pengujian pada Toilet: Kenali Pengujian Anda Ganda.
Pengujian ganda yang paling umum digunakan di Android adalah Fakes dan Mocks.
Dalam tugas ini, Anda akan membuat pengujian FakeDataSource
ganda untuk pengujian unit DefaultTasksRepository
yang dipisahkan dari sumber data aktual.
Langkah 1: Buat class FakeDataSource
Pada langkah ini, Anda akan membuat class yang disebut FakeDataSouce
, yang akan menjadi pengujian ganda dari LocalDataSource
dan RemoteDataSource
.
- Di set sumber test, klik kanan New -> Package.
- Buat paket data dengan paket sumber di dalamnya.
- Buat class baru bernama
FakeDataSource
dalam paket data/sumber.
Langkah 2: Mengimplementasikan Antarmuka TasksDataSource
Agar dapat menggunakan class baru FakeDataSource
sebagai pengujian ganda, class ini harus dapat menggantikan sumber data lainnya. Sumber data tersebut adalah TasksLocalDataSource
dan TasksRemoteDataSource
.
- Perhatikan bahwa 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 melaporkan bahwa Anda belum menerapkan metode yang diperlukan untuk TasksDataSource
.
- Gunakan menu perbaikan cepat dan pilih Terapkan anggota.
- Pilih semua metode, lalu tekan OK.
Langkah 3: Mengimplementasikan metode getTasks di FakeDataSource
FakeDataSource
adalah jenis pengujian ganda khusus yang disebut palsu. Pengujian palsu adalah pengujian ganda yang memiliki implementasi "kerja" kelas, tetapi diimplementasikan dengan cara yang membuatnya bagus untuk pengujian tetapi tidak cocok untuk produksi. "Penerapan" berarti kelas akan menghasilkan keluaran yang realistis dengan masukan yang diberikan.
Misalnya, sumber data palsu tidak akan terhubung ke jaringan atau menyimpan apa pun ke basis data—hanya akan menggunakan daftar dalam memori. Ini akan "bekerja seperti yang Anda harapkan" dalam metode tersebut untuk mendapatkan atau menyimpan tugas akan mengembalikan hasil yang diharapkan, tetapi Anda tidak akan pernah dapat menggunakan implementasi 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 "sangat" untuk pengujian.
- Ubah konstruktor
FakeDataSource
untuk membuatvar
bernamatasks
yang merupakanMutableList<Task>?
dengan nilai default dari daftar kosong yang dapat diubah.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Ini adalah daftar tugas yang "palsu" menjadi respons database atau server. Untuk saat ini, tujuannya adalah untuk menguji metode getTasks
repositori. Metode ini memanggil metode getTasks
deleteAllTasks
, sumber data, 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 diubah. - Tulis
saveTask
: tambahkan tugas ke daftar.
Metode tersebut, yang diimplementasikan untuk FakeDataSource
, 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 serupa 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 pengujian ganda palsu yang baru saja dibuat.
Masalah utamanya adalah Anda memiliki FakeDataSource
, tetapi cara penggunaannya dalam pengujian tidak jelas. Ini perlu menggantikan TasksRemoteDataSource
dan TasksLocalDataSource
, tetapi hanya dalam pengujian. Baik TasksRemoteDataSource
maupun TasksLocalDataSource
merupakan dependensi dari DefaultTasksRepository
, yang berarti bahwa DefaultTasksRepositories
memerlukan atau "bergantung pada" agar class ini dapat berjalan.
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 merupakan hard code. Anda tidak dapat menukar pengujian ganda.
Yang ingin Anda lakukan adalah menyediakan sumber data ini ke class, bukan melakukan hard-coding. Menyediakan dependensi disebut sebagai injeksi dependensi. Ada berbagai cara untuk memberikan dependensi, sehingga jenis injeksi dependensi berbeda.
Injeksi Dependensi Konstruktor memungkinkan Anda menukar pengujian ganda dengan meneruskannya ke dalam konstruktor.
Tidak ada injeksi | Injeksi |
Langkah 1: Gunakan Injeksi Dependensi Konstruktor di DefaultTasksRepository
- Ubah konstruktor
DefaultTaskRepository
dari menggunakanApplication
menjadi mengambil sumber data dan dispatcher coroutine (yang juga perlu Anda tukarkan untuk pengujian - ini dijelaskan lebih detail di bagian tutorial ketiga di 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 meneruskan dependensi, hapus metode
init
. Anda tidak perlu lagi membuat dependensi. - Hapus juga variabel instance lama. Anda menentukannya dalam konstruktor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- Terakhir, update 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 sekarang 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
dan pilih Buat, lalu Uji. - Ikuti petunjuk untuk membuat
DefaultTasksRepositoryTest
di set sumber pengujian. - Di bagian atas class
DefaultTasksRepositoryTest
baru, tambahkan variabel anggota di bawah untuk mewakili data dalam sumber data palsu.
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 pengujian ganda Anda, FakeDataSource
.
- Buat metode yang disebut
createRepository
dan anotasikan dengan@Before
. - Buat instance sumber data palsu 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 ini.
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 Anda memanggilgetTasks
dengantrue
(artinya, Anda harus memuat ulang dari sumber data jarak jauh) yang akan menampilkan data 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 pesan error saat memanggil getTasks:
Langkah 4: Tambahkan runBlockingTest
Error coroutine diharapkan karena getTasks
adalah fungsi suspend
dan Anda perlu meluncurkan coroutine untuk memanggilnya. Untuk itu, Anda memerlukan cakupan coroutine. Untuk mengatasi error ini, Anda perlu menambahkan beberapa dependensi gradle untuk menangani peluncuran coroutine dalam pengujian.
- Tambahkan dependensi yang diperlukan untuk menguji coroutine ke set sumber pengujian dengan 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 dimaksudkan untuk menguji coroutine. Untuk menjalankan pengujian, gunakan fungsi runBlockingTest
. Ini adalah fungsi yang disediakan oleh library pengujian coroutine. Fungsi ini mengambil blok kode, lalu menjalankan blok kode ini dalam konteks coroutine khusus yang berjalan secara sinkron dan segera, yang berarti tindakan akan terjadi dalam urutan deterministik. Pada dasarnya ini membuat coroutine Anda berjalan seperti non-coroutine, sehingga dimaksudkan 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. Ini menunjukkan bahwa Anda tahu bahwa Anda menggunakan API coroutine eksperimental (runBlockingTest
) di class. Tanpa itu, Anda akan mendapatkan peringatan. - Kembali ke
DefaultTasksRepositoryTest
, tambahkanrunBlockingTest
sehingga akan menyelesaikan seluruh pengujian Anda sebagai "blok" kode
Pengujian terakhir ini terlihat seperti kode di bawah ini.
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 dan konfirmasikan bahwa pengujian tersebut berfungsi dan error akan hilang.
Anda baru saja melihat cara menguji unit repositori. Pada langkah berikutnya, Anda akan kembali menggunakan injeksi dependensi dan membuat pengujian lain dua kali—kali ini untuk menunjukkan cara menulis pengujian unit dan integrasi untuk model tampilan Anda.
Pengujian unit hanya menguji class atau metode yang Anda minati. Ini dikenal sebagai pengujian di isolasi, tempat Anda memisahkan "unit" dan hanya menguji kode yang merupakan bagian dari unit tersebut.
Jadi, TasksViewModelTest
hanya boleh menguji kode TasksViewModel
—kode tidak boleh diuji dalam 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 digunakan dalam pengujian Anda.
Dalam tugas ini, Anda menerapkan injeksi dependensi untuk melihat model.
Langkah 1. Membuat Antarmuka TasksRepository
Langkah pertama untuk menggunakan injeksi dependensi konstruktor adalah membuat antarmuka yang sama antara class palsu dan class nyata.
Seperti apa praktiknya? Lihat TasksRemoteDataSource
, TasksLocalDataSource
, dan FakeDataSource
, dan perhatikan bahwa semuanya memiliki antarmuka yang sama: TasksDataSource
. Hal ini memungkinkan Anda mengucapkan dalam konstruktor DefaultTasksRepository
yang Anda ambil di TasksDataSource
.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
Inilah yang memungkinkan kami untuk menukar FakeDataSource
Anda!
Selanjutnya, buat antarmuka untuk DefaultTasksRepository
, seperti yang Anda lakukan untuk sumber data. Ini harus mencakup semua metode publik (platform API publik) DefaultTasksRepository
.
- Buka
DefaultTasksRepository
dan klik kanan pada 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 pengiring dan metode pribadi.
- Klik Refactor. Antarmuka
TasksRepository
baru akan muncul dalam paket data/sumber.
Dan DefaultTasksRepository
sekarang mengimplementasikan TasksRepository
.
- Jalankan aplikasi (bukan pengujian) untuk memastikan semuanya masih berfungsi.
Langkah 2. Membuat FakeTestRepository
Setelah memiliki antarmuka, Anda dapat membuat pengujian DefaultTaskRepository
ganda.
- Dalam set sumber pengujian, dalam data/sumber, buat file Kotlin dan class
FakeTestRepository.kt
, lalu 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 Terapkan anggota.
- Pilih semua metode, lalu tekan OK.
Langkah 3. Mengimplementasikan metode FakeTestRepository
Anda sekarang memiliki class FakeTestRepository
dengan metode "belum diimplementasikan" Serupa dengan cara Anda menerapkan FakeDataSource
, FakeTestRepository
akan didukung oleh struktur data, alih-alih menangani mediasi yang rumit antara sumber data lokal dan jarak jauh.
Perhatikan bahwa FakeTestRepository
tidak perlu menggunakan FakeDataSource
atau hal semacam itu; hanya perlu menampilkan output palsu yang realistis berdasarkan input. Anda akan menggunakan LinkedHashMap
untuk menyimpan daftar tugas dan MutableLiveData
untuk tugas yang dapat diamati.
- Di
FakeTestRepository
, tambahkan variabelLinkedHashMap
yang mewakili 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
}
Implementasikan 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 Anda memiliki beberapa Tasks
di repositori Anda. Anda dapat memanggil saveTask
beberapa kali, tetapi untuk mempermudah, tambahkan metode bantuan khusus untuk pengujian yang memungkinkan Anda menambahkan tugas.
- Tambahkan metode
addTasks
, yang menggunakanvararg
tugas, tambahkan setiap tugas keHashMap
, lalu muat 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 menerapkan beberapa metode utama. Selanjutnya, gunakan ini dalam pengujian Anda.
Dalam tugas ini, Anda 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. Contoh:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Seperti pada kode di atas, Anda menggunakan delegasikan properti viewModel's
yang membuat model tampilan. Untuk mengubah cara pembuatan model tampilan, Anda perlu menambahkan dan menggunakan ViewModelProvider.Factory
. Jika tidak memahami ViewModelProvider.Factory
, Anda dapat mempelajarinya lebih lanjut di sini.
Langkah 1. Membuat dan menggunakan ViewModelFactory di TasksViewModel
Anda dapat memulai dengan mengupdate class dan menguji yang terkait dengan layar Tasks
.
- Buka
TasksViewModel
. - Ubah konstruktor
TasksViewModel
untuk menggunakanTasksRepository
, bukan menyusunnya 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
. Letakkan class factory dalam file yang sama dengan TasksViewModel
, tetapi Anda juga dapat menempatkannya dalam filenya sendiri.
- Di bagian bawah file
TasksViewModel
, di luar class, tambahkanTasksViewModelFactory
yang menggunakanTasksRepository
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 ViewModel
dibuat. Setelah memiliki factory, gunakan setelan tersebut di mana pun Anda membuat model tampilan.
- Update
TasksFragment
untuk menggunakan setelan 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 dan pastikan semuanya masih berfungsi.
Langkah 2. Menggunakan FakeTestRepository di dalam TasksViewModelTest
Sekarang Anda dapat menggunakan repositori palsu, bukan menggunakan repositori sungguhan dalam pengujian model tampilan.
- 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 Pengujian AndroidX
ApplicationProvider.getApplicationContext
, Anda juga dapat menghapus anotasi@RunWith(AndroidJUnit4::class)
. - Jalankan pengujian, pastikan pengujian masih berfungsi.
Dengan menggunakan injeksi dependensi konstruktor, Anda kini telah menghapus DefaultTasksRepository
sebagai dependensi dan menggantinya dengan FakeTestRepository
dalam pengujian.
Langkah 3. Mengupdate juga Fragmen TaskDetail dan ViewModel
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 setelan pabrik.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Jalankan kode dan pastikan semuanya berfungsi.
Anda sekarang dapat menggunakan FakeTestRepository
sebagai pengganti repositori sebenarnya di TasksFragment
dan TasksDetailFragment
.
Berikutnya, Anda akan menulis pengujian integrasi untuk menguji fragmen dan interaksi model tampilan. Anda akan mengetahui apakah kode model tampilan memperbarui UI Anda dengan tepat. Untuk melakukannya, gunakan
- pola ServiceLocator
- library Espresso dan Mockito
Pengujian integrasi menguji interaksi beberapa class untuk memastikan perilaku tersebut sesuai dengan yang diharapkan saat digunakan bersama. Pengujian ini dapat dijalankan secara lokal (test
set sumber) atau sebagai uji instrumentasi (androidTest
set sumber).
Dalam kasus ini, Anda akan mengambil setiap fragmen dan menulis pengujian integrasi untuk fragmen dan model tampilan untuk 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 inti Corekotlinx-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 dalam set sumber androidTest
, gunakan androidTestImplementation
untuk menambahkannya sebagai dependensi.
Langkah 2. Membuat class TaskDetailFragmentTest
TaskDetailFragment
menampilkan informasi tentang satu tugas.
Anda akan memulai dengan menulis pengujian fragmen untuk TaskDetailFragment
karena memiliki fungsi yang cukup dasar dibandingkan dengan fragmen lain.
- Buka
taskdetail.TaskDetailFragment
. - Buat pengujian untuk
TaskDetailFragment
, seperti yang telah Anda lakukan sebelumnya. Terima pilihan default dan masukkan ke dalam 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 &@MediumTest
waktu proses" integrasi (dibandingkan dengan pengujian unit@SmallTest
dan@LargeTest
pengujian menyeluruh yang besar). Hal ini membantu Anda mengelompokkan dan memilih ukuran pengujian yang akan dijalankan.@RunWith(AndroidJUnit4::class)
—Digunakan di class mana pun 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 menggabungkan fragmen dan memberi Anda kontrol langsung atas siklus proses fragmen untuk pengujian. Untuk menulis pengujian fragmen, Anda membuat FragmentScenario
untuk fragmen yang diuji (TaskDetailFragment
).
- Salin pengujian ini ke dalam
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 ini di atas:
- Membuat tugas.
- Membuat
Bundle
, yang merepresentasikan argumen fragmen untuk tugas yang diteruskan ke fragmen). - Fungsi
launchFragmentInContainer
membuatFragmentScenario
, dengan paket dan tema ini.
Pengujian ini belum selesai karena belum 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 harus terjadi.
- Pertama, karena ini adalah uji instrumentasi, pengujian akan berjalan di perangkat fisik Anda (jika terhubung) atau emulator.
- Fragmen ini akan meluncurkan fragmen.
- Perhatikan bagaimana fragmen tidak menavigasi melalui fragmen lain atau memiliki menu yang terkait dengan aktivitas - ini hanya dari fragmen.
Terakhir, perhatikan dengan cermat dan perhatikan bahwa fragmen menyatakan "Tidak ada data" karena tidak berhasil memuat data tugas.
Pengujian Anda harus memuat TaskDetailFragment
(yang telah Anda lakukan) dan menegaskan bahwa data telah dimuat dengan benar. Mengapa tidak ada data? Ini karena Anda membuat tugas, tetapi Anda 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 memberikan repositori palsu ke fragmen menggunakan ServiceLocator
. Ini akan memungkinkan Anda menulis fragmen dan melihat pengujian integrasi model.
Anda tidak dapat menggunakan injeksi dependensi konstruktor di sini, seperti yang Anda lakukan sebelumnya, saat Anda perlu memberikan dependensi ke model tampilan atau repositori. Injeksi dependensi konstruktor mengharuskan Anda membuat class. Fragmen dan aktivitas adalah contoh class yang tidak Anda buat dan umumnya tidak memiliki akses ke konstruktor.
Karena Anda tidak membuat fragmen, Anda tidak dapat menggunakan injeksi dependensi konstruktor untuk menukar pengujian repositori ganda (FakeTestRepository
) dengan fragmen. Sebagai gantinya, gunakan pola Pencari Lokasi Layanan. Pola Pencari Lokasi Layanan merupakan alternatif untuk Injeksi Dependensi. Proses 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 reguler (set sumber main
), semua dependensi ini adalah dependensi aplikasi reguler. Untuk pengujian, Anda perlu memodifikasi Pencari Layanan untuk menyediakan dependensi versi ganda pengujian.
Tidak menggunakan Pencari Lokasi | Menggunakan Pencari Lokasi Layanan |
Untuk aplikasi codelab ini, lakukan hal berikut:
- Buat class Pencari Lokasi Layanan yang dapat membuat dan menyimpan repositori. Secara default, fitur ini membuat repositori "normal"
- Faktorkan ulang kode Anda sehingga saat Anda membutuhkan repositori, gunakan Pencari Lokasi Layanan.
- Di class pengujian, panggil metode pada Pencari Lokasi Layanan yang menukar repositori "normal" dengan pengujian ganda Anda.
Langkah 1. Membuat ServiceLocator
Mari kita buat class ServiceLocator
. Ini akan berada di set sumber utama bersama dengan kode aplikasi lainnya karena digunakan oleh kode aplikasi utama.
Catatan: ServiceLocator
adalah singleton, jadi gunakan kata kunci object
Kotlin untuk class tersebut.
- Buat file ServiceLocator.kt di tingkat atas set sumber utama.
- Tentukan
object
yang disebutServiceLocator
. - 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 akan terlihat seperti di bawah ini.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Saat ini satu-satunya hal yang perlu dilakukan ServiceLocator
Anda adalah mengetahui cara menampilkan TasksRepository
. Ini akan menampilkan DefaultTasksRepository
yang sudah ada atau membuat dan menampilkan DefaultTasksRepository
baru, jika diperlukan.
Tentukan fungsi berikut:
provideTasksRepository
—Baik menyediakan repositori yang sudah ada maupun membuat yang baru. Metode ini harus berupasynchronized
dithis
agar dapat menghindari, saat terjadi beberapa thread yang berjalan, pernah membuat 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 memanggilcreateDataBase
.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 dalam Aplikasi
Anda akan melakukan perubahan pada kode aplikasi utama (bukan pengujian) sehingga Anda dapat membuat repositori di satu tempat, ServiceLocator
Anda.
Penting bahwa Anda hanya pernah membuat satu instance dari class repositori. Untuk memastikan ini, Anda akan menggunakan pencari Layanan di class Aplikasi saya.
- Di tingkat teratas hierarki paket, buka
TodoApplication
lalu buatval
untuk repositori Anda dan 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 dalam 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, sebagai ganti 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 memperoleh 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 memfaktorkan ulang, aplikasi harus 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 harus 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 yang disebut
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 pengujian ganda saat pengujian. Untuk melakukannya, Anda perlu menambahkan kode ke kode ServiceLocator
.
- Buka
ServiceLocator.kt
. - Tandai penyetel untuk
tasksRepository
sebagai@VisibleForTesting
. Anotasi ini adalah cara untuk menyatakan bahwa alasan penyetel bersifat publik adalah karena pengujian.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Baik Anda menjalankan pengujian sendiri atau dalam kelompok pengujian, pengujian tersebut harus berjalan sama persis. Artinya, pengujian tidak boleh memiliki perilaku yang bergantung satu sama lain (artinya, menghindari berbagi objek di antara pengujian).
Karena ServiceLocator
adalah singleton, ia memiliki kemungkinan dibagikan secara tidak sengaja di antara pengujian. Untuk membantu menghindari hal ini, buat metode yang dapat mereset status ServiceLocator
di antara pengujian dengan benar.
- Tambahkan variabel instance yang disebut
lock
dengan nilaiAny
.
ServiceLocator.kt
private val lock = Any()
- Tambahkan metode khusus pengujian yang disebut
resetRepository
yang menghapus database dan menetapkan 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 menggunakan ServiceLocator
.
- Buka
TaskDetailFragmentTest
. - Deklarasikan variabel
lateinit TasksRepository
. - Tambahkan metode penyiapan dan terobosan 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()
dirunBlockingTest
. - Simpan
activeTask
di repositori sebelum meluncurkan fragmen.
repository.saveTask(activeTask)
Pengujian terakhir terlihat seperti kode ini di bawah.
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, kode 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()
.
Sama seperti sebelumnya, Anda akan melihat fragmen, kecuali kali ini, karena Anda menyiapkan repositori dengan benar, fragmen kini menampilkan informasi tugas.
Pada langkah ini, Anda akan menggunakan library pengujian UI Espresso untuk menyelesaikan pengujian integrasi pertama Anda. Anda telah menyusun kode sehingga dapat menambahkan pengujian dengan pernyataan untuk UI Anda. Untuk melakukannya, Anda akan menggunakan library pengujian Espresso.
Espresso membantu Anda:
- Lakukan interaksi 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. Perhatikan Dependensi Gradle
Anda sudah memiliki dependensi Espresso utama karena 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 inti Espresso ini disertakan secara default saat Anda membuat project Android baru. File ini berisi kode pengujian dasar untuk sebagian besar tampilan dan tindakan.
Langkah 2. Menonaktifkan animasi
Pengujian Espresso berjalan pada perangkat sungguhan dan dengan demikian merupakan uji instrumentasi secara alami. Satu masalah yang muncul adalah animasi: Jika animasi mengalami keterlambatan dan Anda mencoba menguji apakah tampilan berada di layar, tetapi masih dianimasikan, Espresso dapat secara tidak sengaja gagal dalam pengujian. Ini bisa membuat pengujian Espresso tidak stabil.
Untuk pengujian UI Espresso, praktik terbaiknya adalah menonaktifkan animasi (juga pengujian Anda akan berjalan lebih cepat).
- Di perangkat pengujian, buka Setelan > Opsi developer.
- Nonaktifkan ketiga setelan ini: Skala animasi jendela, Skala animasi transisi, dan Skala durasi animator.
Langkah 3. Lihat pengujian Espresso
Sebelum menulis pengujian Espresso, lihat beberapa kode Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
Apa yang dilakukan pernyataan ini adalah menemukan tampilan kotak centang dengan ID task_detail_complete_checkbox
, mengkliknya, lalu menegaskan bahwa kotak 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 matcher tampilan lainnya yang dapat Anda cari di dokumentasi.
3. ViewAction
perform(click())
Metode perform
yang menggunakan ViewAction
. ViewAction
adalah sesuatu yang dapat dilakukan terhadap tampilan, misalnya di sini, yang mengklik tampilan.
check(matches(isChecked()))
check
yang memerlukan ViewAssertion
. ViewAssertion
memeriksa atau menyatakan sesuatu tentang tampilan. ViewAssertion
yang paling umum akan Anda gunakan adalah pernyataan matches
. Untuk menyelesaikan pernyataan, gunakan ViewMatcher
lainnya, dalam hal ini isChecked
.
Perlu diketahui bahwa Anda tidak selalu memanggil perform
dan check
di pernyataan Espresso. Anda dapat memiliki pernyataan yang hanya membuat pernyataan menggunakan check
atau hanya melakukan ViewAction
menggunakan perform
.
- Buka
TaskDetailFragmentTest.kt
. - Update 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 muncul setelah komentar
// THEN
akan menggunakan Espresso. Periksa struktur pengujian dan penggunaanwithId
, lalu periksa untuk membuat pernyataan tentang bagaimana halaman detail seharusnya terlihat. - Jalankan pengujian dan konfirmasikan bahwa pengujian berhasil.
Langkah 4. Opsional, Tulis Pengujian Espresso Anda sendiri
Sekarang tulis pengujian Anda sendiri.
- Buat pengujian baru yang disebut
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
}
- Berdasarkan pengujian sebelumnya, selesaikan pengujian ini.
- Jalankan dan konfirmasikan bahwa pengujian lulus.
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 navigasi, menggunakan jenis pengujian ganda yang disebut tiruan, dan library pengujian Mockito.
Dalam codelab ini, Anda telah menggunakan pengujian ganda yang disebut palsu. Palsu adalah salah satu dari banyak jenis pengujian ganda. Pengujian ganda mana yang harus Anda gunakan untuk menguji Komponen navigasi?
Pikirkan bagaimana navigasi terjadi. Bayangkan menekan salah satu tugas di TasksFragment
untuk membuka layar detail tugas.
Berikut adalah kode dalam 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 adanya panggilan ke metode navigate
. Jika Anda perlu menulis pernyataan yang tegas, tidak ada cara sederhana untuk menguji apakah Anda telah membuka TaskDetailFragment
. Menavigasi adalah tindakan rumit yang tidak menghasilkan output atau perubahan status yang jelas, selain menginisialisasi TaskDetailFragment
.
Apa yang dapat Anda tegaskan adalah bahwa metode navigate
dipanggil dengan parameter tindakan yang benar. Hal ini persis seperti yang dilakukan oleh pengujian ganda tiruan—ini memeriksa apakah metode tertentu dipanggil.
Mockito adalah framework untuk membuat pengujian ganda. Meskipun kata tiruan digunakan di API dan nama, kata tersebut bukan hanya untuk membuat tiruan. Ini juga bisa membuat stub dan mata-mata.
Anda akan menggunakan Mockito untuk membuat NavigationController
tiruan yang dapat menegaskan 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 menghasilkan class pada waktu proses. Di Android, hal ini dilakukan menggunakan kode byte dex, sehingga library ini memungkinkan Mockito untuk menghasilkan objek selama runtime di Android.androidx.test.espresso:espresso-contrib
—Library ini terdiri dari kontribusi eksternal (dengan nama ini) yang berisi kode pengujian untuk tampilan yang lebih canggih, sepertiDatePicker
danRecyclerView
. Class ini juga berisi class Aksesibilitas yang disebutCountingIdlingResource
yang akan dibahas nanti.
Langkah 2. Membuat TasksFragmentTest
- Buka
TasksFragment
. - Klik kanan pada nama class
TasksFragment
lalu pilih Buat lalu Uji. 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. Ini akan menyiapkan dan menghapus FakeAndroidTestRepository
. Tambahkan pengujian navigasi untuk menguji bahwa saat Anda mengklik tugas di 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 meniru tiruan Mockito, teruskan kelas yang ingin Anda tiru.
Selanjutnya, Anda perlu mengaitkan NavController
Anda dengan fragmen. onFragment
memungkinkan Anda memanggil metode pada fragmen itu sendiri.
- Buat tiruan baru
NavController
.
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 di 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
tiruan adalah apa yang membuat ini tiruan—Anda dapat mengonfirmasi navController
tiruan yang disebut metode tertentu (navigate
) dengan parameter (actionTasksFragmentToTaskDetailFragment
dengan ID "id1").
Pengujian lengkap 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
tiruan tersebut ke fragmen. - Pastikan bahwa navigasi dipanggil dengan tindakan dan parameter yang benar.
Langkah 3. Opsional, tulis clickAddTaskButton_navigateToAddEditFragment
Untuk mengetahui apakah Anda dapat menulis pengujian navigasi sendiri, coba tugas ini.
- Tulis pengujian
clickAddTaskButton_navigateToAddEditFragment
yang memeriksa apakah Anda mengklik FAB +, dan membukaAddEditTaskFragment
.
Jawabannya ada di bawah ini.
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 mencakup cara menyiapkan injeksi dependensi manual, pencari lokasi layanan, dan cara menggunakan tiruan dan tiruan di aplikasi Kotlin Android. Khususnya:
- Hal yang ingin Anda uji dan strategi pengujian menentukan jenis pengujian yang akan diterapkan untuk aplikasi Anda. Pengujian unit difokuskan dan cepat. Pengujian integrasi memverifikasi interaksi antara bagian program Anda. Pengujian menyeluruh memverifikasi fitur, memiliki fidelitas tertinggi, sering diinstrumentasi, dan mungkin perlu waktu lebih lama untuk dijalankan.
- Arsitektur aplikasi memengaruhi seberapa sulit pengujian itu.
- TDD atau Pengembangan Berdasarkan Pengujian adalah strategi tempat Anda menulis pengujian terlebih dahulu, lalu membuat fitur untuk lulus pengujian.
- Untuk mengisolasi bagian aplikasi untuk pengujian, Anda dapat menggunakan pengujian ganda. Pengujian ganda adalah versi class yang dibuat khusus untuk pengujian. Misalnya, Anda berpura-pura mendapatkan data dari database atau internet.
- Gunakan injeksi dependensi untuk mengganti class sungguhan dengan class pengujian, misalnya, repositori atau lapisan jaringan.
- Gunakan pengujian yang ditentukan (
androidTest
) untuk meluncurkan komponen UI. - Jika tidak dapat menggunakan injeksi dependensi konstruktor, misalnya untuk meluncurkan fragmen, Anda sering kali dapat menggunakan pencari lokasi layanan. Pola Pencari Lokasi Layanan adalah alternatif untuk Injeksi Dependensi. Proses 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
- Library Pengujian AndroidX
- Library Pengujian Inti Komponen Arsitektur AndroidX
- Set sumber
- Menguji dari command line
Video:
Lainnya:
Untuk link ke codelab lainnya dalam kursus ini, lihat halaman landing codelab Android Lanjutan di Kotlin.