Hai, Habr. Nama saya Ilya Smirnov, saya adalah pengembang Android di FINCH. Saya ingin menunjukkan kepada Anda beberapa contoh bekerja dengan tes Unit yang telah kami kembangkan di tim kami.
Dua jenis pengujian Unit digunakan dalam proyek kami: pemeriksaan kesesuaian dan pemeriksaan panggilan. Mari kita bahas masing-masing dengan lebih detail.
Pengujian kepatuhan
Pengujian kesesuaian memeriksa apakah hasil aktual dari pelaksanaan beberapa fungsi sesuai dengan hasil yang diharapkan atau tidak. Mari saya tunjukkan sebuah contoh - bayangkan ada aplikasi yang menampilkan daftar berita untuk hari itu:

Data tentang berita diambil dari sumber yang berbeda dan pada saat keluar dari lapisan bisnis diubah menjadi model berikut:
data class News( val text: String, val date: Long )
Menurut logika aplikasi, model formulir berikut ini diperlukan untuk setiap elemen daftar:
data class NewsViewData( val id: String, val title: String, val description: String, val date: String )
Kelas berikut akan bertanggung jawab untuk mengonversi model
domain ke model
tampilan :
class NewsMapper { fun mapToNewsViewData(news: List<News>): List<NewsViewData> { return mutableListOf<NewsViewData>().apply{ news.forEach { val textSplits = it.text.split("\\.".toRegex()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale("ru")) add( NewsViewData( id = it.date.toString(), title = textSplits[0], description = textSplits[1].trim(), date = dateFormat.format(it.date) ) ) } } } }
Dengan demikian, kita tahu bahwa beberapa objek
News( "Super News. Some description and bla bla bla", 1551637424401 )
Akan dikonversi ke beberapa objek
NewsViewData( "1551637424401", "Super News", "Some description and bla bla bla", "2019-03-03 21:23" )
Data input dan output diketahui, yang berarti Anda dapat menulis tes untuk metode
mapToNewsViewData , yang akan memeriksa kepatuhan data output tergantung pada input.
Untuk melakukan ini, di folder app / src / test / ..., buat kelas
NewsMapperTest dengan konten berikut:
class NewsMapperTest { private val mapper = NewsMapper() @Test fun mapToNewsViewData() { val inputData = listOf( News("Super News. Some description and bla bla bla", 1551637424401) ) val outputData = mapper.mapToNewsViewData(inputData) Assert.assertEquals(outputData.size, inputData.size) outputData.forEach { Assert.assertEquals(it.id, "1551637424401") Assert.assertEquals(it.title, "Super News") Assert.assertEquals(it.description, "Some description and bla bla bla") Assert.assertEquals(it.date, "2019-03-03 21:23") } } }
Hasil yang diperoleh dibandingkan dengan harapan menggunakan metode dari paket
org.junit.Assert . Jika ada nilai yang tidak memenuhi harapan, maka tes akan gagal.
Ada kalanya konstruktor dari kelas yang diuji mengambil beberapa dependensi. Ini bisa berupa ResourceManager sederhana untuk mengakses sumber daya, atau
Interactor penuh untuk menjalankan logika bisnis. Anda dapat membuat turunan dari ketergantungan seperti itu, tetapi lebih baik untuk membuat objek tiruan serupa. Objek tiruan memberikan implementasi fiktif kelas, yang dengannya Anda dapat melacak panggilan metode internal dan menimpa nilai kembali.
Ada kerangka kerja
Mockito yang populer untuk membuat tiruan.
Di Kotlin, semua kelas adalah final secara default, jadi Anda tidak dapat membuat objek tiruan di Mockito dari awal. Untuk mengatasi keterbatasan ini, Anda disarankan untuk menambahkan ketergantungan
mockito-inline .
Jika Anda menggunakan kotlin dsl saat menulis tes, Anda dapat menggunakan berbagai perpustakaan, seperti
Mockito-Kotlin .
Misalkan
NewsMapper mengambil dalam bentuk suatu ketergantungan
NewsRepo tertentu, yang mencatat informasi tentang pengguna yang melihat
item berita tertentu. Maka masuk akal untuk mengejek
NewsRepo dan memeriksa nilai
pengembalian metode
mapToNewsViewData tergantung pada hasil
isNewsRead .
class NewsMapperTest { private val newsRepo: NewsRepo = mock() private val mapper = NewsMapper(newsRepo) … @Test fun mapToNewsViewData_Read() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(true) ... } @Test fun mapToNewsViewData_UnRead() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(false) ... } … }
Dengan demikian, objek tiruan memungkinkan Anda untuk mensimulasikan berbagai opsi untuk nilai kembali untuk menguji berbagai kasus uji.
Selain contoh di atas, pengujian kesesuaian mencakup berbagai validator data. Misalnya, metode yang memeriksa kata sandi yang dimasukkan untuk keberadaan karakter khusus dan panjang minimum.
Uji Panggilan
Menguji panggilan memeriksa apakah metode satu kelas memanggil metode yang diperlukan dari kelas lain atau tidak. Paling sering, pengujian semacam itu diterapkan pada
Presenter , yang mengirimkan perintah Lihat khusus untuk mengubah keadaan. Kembali ke contoh daftar berita:
class MainPresenter( private val view: MainView, private val interactor: NewsInteractor, private val mapper: NewsMapper ) { var scope = CoroutineScope(Dispatchers.Main) fun onCreated() { view.setLoading(true) scope.launch { val news = interactor.getNews() val newsData = mapper.mapToNewsViewData(news) view.setLoading(false) view.setNewsItems(newsData) } } … }
Hal yang paling penting di sini adalah fakta untuk menggunakan metode dari
Interactor dan
View . Tes akan terlihat seperti ini:
class MainPresenterTest { private val view: MainView = mock() private val mapper: NewsMapper = mock() private val interactor: NewsInteractor = mock() private val presenter = MainPresenter(view, interactor, mapper).apply { scope = CoroutineScope(Dispatchers.Unconfined) } @Test fun onCreated() = runBlocking { whenever(interactor.getNews()).doReturn(emptyList()) whenever(mapper.mapToNewsViewData(emptyList())).doReturn(emptyList()) presenter.onCreated() verify(view, times(1)).setLoading(true) verify(interactor).getNews() verify(mapper).mapToNewsViewData(emptyList()) verify(view).setLoading(false) verify(view).setNewsItems(emptyList()) } }
Solusi yang berbeda mungkin diperlukan untuk mengecualikan dependensi platform dari pengujian, seperti itu semua tergantung pada teknologi untuk bekerja dengan multithreading. Contoh di atas menggunakan Kotlin Coroutines dengan ruang lingkup ditimpa untuk menjalankan tes, seperti digunakan dalam kode program Dispatchers.Main mengacu pada utas UI android, yang tidak dapat diterima dalam jenis pengujian ini. Menggunakan RxJava akan membutuhkan solusi lain, misalnya, membuat TestRule yang mengalihkan aliran eksekusi kode.
Untuk memverifikasi bahwa suatu metode telah dipanggil, metode verifikasi digunakan, yang dapat mengambil metode yang menunjukkan jumlah panggilan ke metode yang sedang diuji sebagai argumen tambahan.
*****
Opsi pengujian yang dipertimbangkan dapat mencakup persentase kode yang cukup besar, membuat aplikasi lebih stabil dan dapat diprediksi. Kode yang dicakup dengan tes lebih mudah dirawat, lebih mudah untuk diukur, karena Ada sejumlah keyakinan bahwa ketika menambahkan fungsionalitas baru, tidak ada yang akan rusak. Dan tentu saja, kode seperti itu lebih mudah direvisi.
Kelas termudah untuk diuji tidak mengandung dependensi platform, karena ketika bekerja dengannya, Anda tidak perlu solusi pihak ketiga untuk membuat objek tiruan platform. Oleh karena itu, proyek kami menggunakan arsitektur yang meminimalkan penggunaan dependensi platform pada lapisan yang diuji.
Kode yang baik harus diuji. Kompleksitas atau ketidakmampuan untuk menulis unit test biasanya menunjukkan bahwa ada sesuatu yang salah dengan kode yang diuji, dan inilah saatnya untuk memikirkan refactoring.
Kode sumber untuk contoh tersedia di
GitHub .