Pengantar Duplikat Pengujian dan Injeksi Dependensi

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:

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:

Download Zip

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:

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: com.example.android.architecture.blueprints.todoapp

.addedittask

Layar menambahkan atau mengedit tugas: Kode lapisan UI untuk menambahkan atau mengedit tugas.

.data

Lapisan data: Bagian ini berhubungan dengan lapisan data tugas. Direktori ini berisi kode database, jaringan, dan repositori.

.statistics

Layar statistik: Kode lapisan UI untuk layar statistik.

.taskdetail

Layar detail tugas: Kode lapisan UI untuk satu tugas.

.tasks

Layar tugas: Kode lapisan UI untuk daftar semua tugas.

.util

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:

  1. Pertama, Anda akan melakukan pengujian unit pada repositori.
  2. Kemudian, Anda akan menggunakan test double di model tampilan, yang diperlukan untuk pengujian unit dan pengujian integrasi model tampilan.
  3. Selanjutnya, Anda akan mempelajari cara menulis pengujian integrasi untuk fragmen dan model tampilannya.
  4. 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. StubTaskRepository dapat diprogram untuk menampilkan kombinasi tugas tertentu dari getTasks misalnya.

Dummy

Pengganti pengujian yang diteruskan tetapi tidak digunakan, seperti jika Anda hanya perlu memberikannya sebagai parameter. Jika Anda memiliki NoOpTaskRepository, NoOpTaskRepository tersebut hanya akan menerapkan TaskRepository dengan tidak ada kode di salah satu metode.

Spy

Pengujian ganda yang juga melacak beberapa informasi tambahan; misalnya, jika Anda membuat SpyTaskRepository, pengujian ganda tersebut dapat melacak jumlah panggilan metode addTask.

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.

  1. Di set sumber test, klik kanan, lalu pilih New -> Package.

  1. Buat paket data dengan paket sumber di dalamnya.
  2. 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.

  1. Perhatikan bagaimana keduanya menerapkan antarmuka TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Buat FakeDataSource mengimplementasikan TasksDataSource:
class FakeDataSource : TasksDataSource {

}

Android Studio akan mengeluh bahwa Anda belum menerapkan metode yang diperlukan untuk TasksDataSource.

  1. Gunakan menu perbaikan cepat, lalu pilih Implement members.


  1. 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.
  1. Ubah konstruktor FakeDataSource untuk membuat var bernama tasks yang merupakan MutableList<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:

  1. Tulis getTasks: Jika tasks bukan null, tampilkan hasil Success. Jika tasks adalah null, tampilkan hasil Error.
  2. Tulis deleteAllTasks: hapus daftar tugas yang dapat berubah.
  3. 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

  1. Ubah konstruktor DefaultTaskRepository dari yang menerima Application 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 }
  1. Karena Anda telah meneruskan dependensi, hapus metode init. Anda tidak perlu lagi membuat dependensi.
  2. 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
  1. 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.

  1. Klik kanan nama class DefaultTasksRepository, pilih Generate, lalu Test.
  2. Ikuti petunjuk untuk membuat DefaultTasksRepositoryTest di set sumber test.
  3. 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 }
  1. Buat tiga variabel, dua variabel anggota FakeDataSource (satu untuk setiap sumber data untuk repositori Anda) dan variabel untuk DefaultTasksRepository 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.

  1. Buat metode dengan nama createRepository dan anotasikan dengan @Before.
  2. Buat instance sumber data palsu Anda, menggunakan daftar remoteTasks dan localTasks.
  3. Buat instance tasksRepository, menggunakan dua sumber data palsu yang baru saja Anda buat dan Dispatchers.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.

  1. Tulis pengujian untuk metode getTasks repositori. Periksa apakah saat Anda memanggil getTasks dengan true (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.

  1. 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.

  1. 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.
  2. Kembali di DefaultTasksRepositoryTest, tambahkan runBlockingTest 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))
    }

}
  1. 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.

  1. Buka DefaultTasksRepository dan klik kanan nama class. Kemudian, pilih Refactor -> Extract -> Interface.

  1. Pilih Ekstrak ke file terpisah.

  1. Di jendela Extract Interface, ubah nama antarmuka menjadi TasksRepository.
  2. Di bagian Members to form interface, centang semua anggota kecuali dua anggota pendamping dan metode pribadi.


  1. Klik Refactor. Antarmuka TasksRepository baru akan muncul di paket data/source .

Selain itu, DefaultTasksRepository sekarang mengimplementasikan TasksRepository.

  1. 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.

  1. Di set sumber test, di data/source, buat file dan class Kotlin FakeTestRepository.kt dan perluas dari antarmuka TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Anda akan diberi tahu bahwa Anda perlu menerapkan metode antarmuka.

  1. Arahkan kursor ke error hingga Anda melihat menu saran, lalu klik dan pilih Implement members.
  1. 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.

  1. Di FakeTestRepository, tambahkan variabel LinkedHashMap yang merepresentasikan daftar tugas saat ini dan MutableLiveData 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:

  1. getTasks—Metode ini harus mengambil tasksServiceData dan mengubahnya menjadi daftar menggunakan tasksServiceData.values.toList(), lalu menampilkannya sebagai hasil Success.
  2. refreshTasks—Memperbarui nilai observableTasks menjadi nilai yang ditampilkan oleh getTasks().
  3. observeTasks—Membuat coroutine menggunakan runBlocking dan menjalankan refreshTasks, lalu menampilkan observableTasks.

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.

  1. Tambahkan metode addTasks, yang mengambil vararg tugas, menambahkan setiap tugas ke HashMap, 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.

  1. Buka TasksViewModel.
  2. Ubah konstruktor TasksViewModel untuk menggunakan TasksRepository, 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.

  1. Di bagian bawah file TasksViewModel, di luar class, tambahkan TasksViewModelFactory yang mengambil TasksRepository 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.

  1. 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))
}
  1. 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.

  1. Buka TasksViewModelTest.
  2. Tambahkan properti FakeTestRepository di TasksViewModelTest.

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
}
  1. Perbarui metode setupViewModel untuk membuat FakeTestRepository dengan tiga tugas, lalu buat tasksViewModel 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)
        
    }
  1. Karena Anda tidak lagi menggunakan kode AndroidX Test ApplicationProvider.getApplicationContext, Anda juga dapat menghapus anotasi @RunWith(AndroidJUnit4::class).
  2. 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.

  1. Buka TaskDetailViewModel.
  2. 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 }
  1. Di bagian bawah file TaskDetailViewModel, di luar class, tambahkan TaskDetailViewModelFactory.

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)
}
  1. 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))
}
  1. 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

  1. 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 inti
  • kotlinx-coroutines-test—Library pengujian coroutine
  • androidx.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.

  1. Buka taskdetail.TaskDetailFragment.
  2. Buat pengujian untuk TaskDetailFragment, seperti yang telah Anda lakukan sebelumnya. Terima pilihan default dan masukkan ke set sumber androidTest (BUKAN set sumber test).

  1. 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).

  1. 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:

Ini belum merupakan pengujian yang selesai, karena tidak menegaskan apa pun. Untuk saat ini, jalankan pengujian dan amati apa yang terjadi.

  1. Ini adalah pengujian berinstrumen, jadi pastikan emulator atau perangkat Anda terlihat.
  2. 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:

  1. Buat class Service Locator yang dapat membuat dan menyimpan repositori. Secara default, perintah ini membuat repositori "normal".
  2. Faktorkan ulang kode Anda sehingga saat Anda memerlukan repositori, gunakan Pencari Layanan.
  3. 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.

  1. Buat file ServiceLocator.kt di tingkat teratas set sumber utama.
  2. Tentukan object bernama ServiceLocator.
  3. Buat variabel instance database dan repository, lalu tetapkan keduanya ke null.
  4. 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:

  1. provideTasksRepository—Menyediakan repositori yang sudah ada atau membuat yang baru. Metode ini harus synchronized pada this untuk menghindari, dalam situasi dengan beberapa thread yang berjalan, pembuatan dua instance repositori secara tidak sengaja.
  2. createTasksRepository—Kode untuk membuat repositori baru. Akan memanggil createTaskLocalDataSource dan membuat TasksRemoteDataSource baru.
  3. createTaskLocalDataSource—Kode untuk membuat sumber data lokal baru. Akan menelepon createDataBase.
  4. 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.

  1. Di level teratas hierarki paket, buka TodoApplication dan buat val untuk repositori Anda, lalu tetapkan repositori yang diperoleh menggunakan ServiceLocator.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.

  1. 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.

  1. Buka TaskDetailFragement dan temukan panggilan ke getRepository di bagian atas class.
  2. 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)
}
  1. 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)
    }
  1. Untuk StatisticsViewModel dan AddEditTaskViewModel, perbarui kode yang mendapatkan repositori untuk menggunakan repositori dari TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. 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.

  1. Klik kanan set sumber androidTest dan buat paket data. Klik kanan lagi dan buat paket sumber .
  2. Buat class baru dalam paket sumber ini bernama FakeAndroidTestRepository.kt.
  3. 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.

  1. Buka ServiceLocator.kt.
  2. 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.

  1. Tambahkan variabel instance bernama lock dengan nilai Any.

ServiceLocator.kt

private val lock = Any()
  1. 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.

  1. Buka TaskDetailFragmentTest.
  2. Deklarasikan variabel lateinit TasksRepository.
  3. 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()
    }
  1. Gabungkan isi fungsi activeTaskDetails_DisplayedInUi() dalam runBlockingTest.
  2. 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)

    }
  1. 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)

    }

}
  1. 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):

  1. Di perangkat pengujian, buka Setelan > Opsi developer.
  2. 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:

1. Metode Espresso statis

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.

4. ViewAssertion

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.

  1. Buka TaskDetailFragmentTest.kt.
  2. 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
  1. Semua yang ada setelah komentar // THEN menggunakan Espresso. Periksa struktur pengujian dan penggunaan withId serta periksa untuk membuat pernyataan tentang tampilan halaman detail.
  2. Jalankan pengujian dan konfirmasi bahwa pengujian berhasil.

Langkah 4. Opsional, Menulis Pengujian Espresso Anda Sendiri

Sekarang tulis pengujian Anda sendiri.

  1. 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
}
  1. Melihat pengujian sebelumnya, selesaikan pengujian ini.
  2. 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

  1. 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, seperti DatePicker dan RecyclerView. File ini juga berisi pemeriksaan Aksesibilitas dan class bernama CountingIdlingResource yang akan dibahas nanti.

Langkah 2. Buat TasksFragmentTest

  1. Buka TasksFragment.
  2. Klik kanan nama class TasksFragment, pilih Generate, lalu Test. Buat pengujian di set sumber androidTest.
  3. 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.

  1. 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)
        
    }
  1. 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.

  1. Jadikan tiruan baru Anda NavController fragmen.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. 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.

  1. 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")
    )
}
  1. Jalankan pengujian.

Singkatnya, untuk menguji navigasi, Anda dapat:

  1. Gunakan Mockito untuk membuat tiruan NavController.
  2. Lampirkan NavController yang di-mock tersebut ke fragmen.
  3. 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.

  1. Tulis pengujian clickAddTaskButton_navigateToAddEditFragment yang memeriksa apakah jika Anda mengklik FAB +, Anda akan membuka AddEditTaskFragment.

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.

Download Zip

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:

Video:

Lainnya:

Untuk mengetahui link ke codelab lain dalam kursus ini, lihat halaman landing codelab Android Lanjutan di Kotlin.