Pengantar Pengujian Ganda dan Injeksi Dependensi

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:

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:

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

Anda harus memahami arsitektur umum aplikasi daripada memiliki pemahaman mendalam tentang logika di satu lapisan.

Berikut ringkasan paket yang akan Anda temukan:

Paket: com.example.android.architecture.blueprints.todoapp

.addedittask

Menambahkan atau mengedit layar tugas: Kode lapisan UI untuk menambahkan atau mengedit tugas.

.data

Lapisan data: Hal ini berkaitan dengan lapisan data tugas. Project ini berisi database, jaringan, dan kode 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 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:

  1. Pertama-tama, Anda akan menguji unit repositori.
  2. Kemudian, Anda akan menggunakan pengujian ganda dalam model tampilan, yang diperlukan untuk pengujian unit dan pengujian integrasi model tampilan.
  3. Berikutnya, Anda akan belajar menulis pengujian integrasi untuk fragmen dan model tampilannya.
  4. 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, StubTaskRepository dapat diprogram untuk menampilkan kombinasi tugas tertentu dari getTasks.

Dummy

Pengujian ganda yang diteruskan tetapi tidak digunakan, seperti jika Anda hanya perlu menyediakannya sebagai parameter. Jika Anda memiliki NoOpTaskRepository, kode hanya akan mengimplementasikan TaskRepository dengan kode tidak di salah satu metode.

Mata-mata

Pengujian ganda yang juga melacak beberapa informasi tambahan; misalnya, jika Anda membuat SpyTaskRepository, pengujian ini dapat melacak frekuensi metode addTask dipanggil.

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.

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

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

  1. Perhatikan bahwa 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 melaporkan bahwa Anda belum menerapkan metode yang diperlukan untuk TasksDataSource.

  1. Gunakan menu perbaikan cepat dan pilih Terapkan anggota.


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

  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 diubah.
  3. 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

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

  1. Klik kanan nama class DefaultTasksRepository dan pilih Buat, lalu Uji.
  2. Ikuti petunjuk untuk membuat DefaultTasksRepositoryTest di set sumber pengujian.
  3. 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 }
  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 pengujian ganda Anda, FakeDataSource.

  1. Buat metode yang disebut createRepository dan anotasikan dengan @Before.
  2. Buat instance sumber data palsu 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 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!

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

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

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

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

  1. Buka DefaultTasksRepository dan klik kanan pada 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 pengiring dan metode pribadi.


  1. Klik Refactor. Antarmuka TasksRepository baru akan muncul dalam paket data/sumber.

Dan DefaultTasksRepository sekarang mengimplementasikan TasksRepository.

  1. Jalankan aplikasi (bukan pengujian) untuk memastikan semuanya masih berfungsi.

Langkah 2. Membuat FakeTestRepository

Setelah memiliki antarmuka, Anda dapat membuat pengujian DefaultTaskRepository ganda.

  1. Dalam set sumber pengujian, dalam data/sumber, buat file Kotlin dan class FakeTestRepository.kt, lalu 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 Terapkan anggota.
  1. 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.

  1. Di FakeTestRepository, tambahkan variabel LinkedHashMap yang mewakili 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
}

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

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

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

  1. Di bagian bawah file TasksViewModel, di luar class, tambahkan TasksViewModelFactory yang menggunakan 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 ViewModel dibuat. Setelah memiliki factory, gunakan setelan tersebut di mana pun Anda membuat model tampilan.

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

  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 Pengujian AndroidX ApplicationProvider.getApplicationContext, Anda juga dapat menghapus anotasi @RunWith(AndroidJUnit4::class).
  2. 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.

  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 setelan 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 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

  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 inti Core
  • 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 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.

  1. Buka taskdetail.TaskDetailFragment.
  2. Buat pengujian untuk TaskDetailFragment, seperti yang telah Anda lakukan sebelumnya. Terima pilihan default dan masukkan ke dalam 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 &@MediumTestwaktu 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).

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

Pengujian ini belum selesai karena belum 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 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:

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

  1. Buat file ServiceLocator.kt di tingkat atas set sumber utama.
  2. Tentukan object yang disebut 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 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:

  1. provideTasksRepository—Baik menyediakan repositori yang sudah ada maupun membuat yang baru. Metode ini harus berupa synchronized di this agar dapat menghindari, saat terjadi beberapa thread yang berjalan, pernah membuat 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 memanggil 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 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.

  1. Di tingkat teratas hierarki paket, buka TodoApplication lalu buat val untuk repositori Anda dan 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 dalam 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, sebagai ganti 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 memperoleh 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 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.

  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 yang disebut 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 pengujian ganda saat pengujian. Untuk melakukannya, Anda perlu menambahkan kode ke kode ServiceLocator.

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

  1. Tambahkan variabel instance yang disebut lock dengan nilai Any.

ServiceLocator.kt

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

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

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

    }

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

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

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

4. ViewAssertion

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.

  1. Buka TaskDetailFragmentTest.kt.
  2. 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
  1. Semua yang muncul setelah komentar // THEN akan menggunakan Espresso. Periksa struktur pengujian dan penggunaan withId, lalu periksa untuk membuat pernyataan tentang bagaimana halaman detail seharusnya terlihat.
  2. Jalankan pengujian dan konfirmasikan bahwa pengujian berhasil.

Langkah 4. Opsional, Tulis Pengujian Espresso Anda sendiri

Sekarang tulis pengujian Anda sendiri.

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

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

Langkah 2. Membuat TasksFragmentTest

  1. Buka TasksFragment.
  2. Klik kanan pada nama class TasksFragment lalu pilih Buat lalu Uji. 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. 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.

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

  1. Buat tiruan baru NavController.
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 di 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 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")
    )
}
  1. Jalankan pengujian.

Singkatnya, untuk menguji navigasi, Anda dapat:

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

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

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.

Download Zip

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:

Video:

Lainnya:

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