Cara Menulis Tes Otomatis untuk iOS
Diterbitkan: 2022-03-11Sebagai pengembang yang baik, Anda melakukan yang terbaik untuk menguji semua fungsionalitas dan setiap kemungkinan jalur kode dan hasil dalam perangkat lunak yang Anda tulis. Tetapi sangat jarang dan tidak biasa untuk dapat menguji secara manual setiap hasil yang mungkin dan setiap jalur yang mungkin diambil pengguna.
Saat aplikasi menjadi lebih besar dan lebih kompleks, kemungkinan Anda akan melewatkan sesuatu melalui pengujian manual meningkat secara signifikan.
Pengujian otomatis, baik UI maupun API layanan back-end, akan membuat Anda lebih yakin bahwa semuanya berfungsi sebagaimana mestinya dan akan mengurangi stres saat mengembangkan, memfaktorkan ulang, menambahkan fitur baru, atau mengubah yang sudah ada.
Dengan pengujian otomatis, Anda dapat:
- Kurangi bug: Tidak ada metode yang akan sepenuhnya menghilangkan kemungkinan bug dalam kode Anda, tetapi pengujian otomatis dapat sangat mengurangi jumlah bug.
- Buat perubahan dengan percaya diri: Hindari bug saat menambahkan fitur baru, yang berarti, Anda dapat membuat perubahan dengan cepat dan tanpa rasa sakit.
- Dokumentasikan kode kita: Saat melihat melalui pengujian, kita dapat dengan jelas melihat apa yang diharapkan dari fungsi tertentu, apa kondisinya, dan apa kasus sudutnya.
- Refactor tanpa rasa sakit: Sebagai pengembang, terkadang Anda mungkin takut melakukan refactoring, terutama jika Anda perlu melakukan refactor sejumlah besar kode. Tes unit ada di sini untuk memastikan bahwa kode refactored masih berfungsi sebagaimana dimaksud.
Artikel ini mengajarkan Anda cara menyusun dan menjalankan pengujian otomatis pada platform iOS.
Tes Unit vs. Tes UI
Sangat penting untuk membedakan antara pengujian unit dan UI.
Tes unit menguji fungsi tertentu di bawah konteks tertentu . Tes unit memverifikasi bahwa bagian kode yang diuji (biasanya satu fungsi) melakukan apa yang seharusnya dilakukan. Ada banyak buku dan artikel tentang unit test, jadi kami tidak akan membahasnya di posting ini.
Tes UI adalah untuk menguji antarmuka pengguna. Misalnya, ini memungkinkan Anda menguji apakah tampilan diperbarui sebagaimana dimaksud atau tindakan tertentu dipicu sebagaimana mestinya saat pengguna berinteraksi dengan elemen UI tertentu.
Setiap pengujian UI menguji interaksi pengguna tertentu dengan UI aplikasi. Pengujian otomatis dapat, dan harus, dilakukan pada tingkat pengujian unit dan pengujian UI.
Menyiapkan Tes Otomatis
Karena XCode mendukung pengujian unit dan UI di luar kotak, mudah dan mudah untuk menambahkannya ke proyek Anda. Saat membuat proyek baru, cukup centang "Sertakan Tes Unit" dan "Sertakan Tes UI."
Saat proyek dibuat, dua target baru akan ditambahkan ke proyek Anda ketika dua opsi ini telah dicentang. Nama target baru memiliki "Tes" atau "UITests" yang ditambahkan di akhir nama.
Itu dia. Anda siap untuk menulis pengujian otomatis untuk proyek Anda.
Jika Anda sudah memiliki proyek yang ada dan ingin menambahkan dukungan pengujian UI dan Unit, Anda harus melakukan sedikit lebih banyak pekerjaan, tetapi juga sangat mudah dan sederhana.
Buka File → Baru → Target dan pilih Bundel Pengujian Unit iOS untuk pengujian Unit atau Bundel Pengujian UI iOS untuk pengujian UI.
Tekan Berikutnya .
Di layar opsi target, Anda dapat membiarkan semuanya apa adanya (jika Anda memiliki beberapa target dan hanya ingin menguji target tertentu, pilih target di dropdown Target yang akan diuji).
Tekan Selesai . Ulangi langkah ini untuk pengujian UI, dan Anda akan memiliki segalanya untuk mulai menulis pengujian otomatis di proyek yang sudah ada.
Tes Unit Menulis
Sebelum kita dapat mulai menulis unit test, kita harus memahami anatominya. Saat Anda menyertakan pengujian unit dalam proyek Anda, contoh kelas pengujian akan dibuat. Dalam kasus kami, itu akan terlihat seperti ini:
import XCTest class TestingIOSTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } }
Metode yang paling penting untuk dipahami adalah setUp
dan tearDown
. Metode setUp
dipanggil sebelum setiap metode pengujian, sedangkan metode tearDown
dipanggil setelah setiap metode pengujian. Jika kita menjalankan tes yang ditentukan dalam contoh kelas tes ini, metode akan berjalan seperti ini:
setUp → testExample → tearDown setUp → testPerformanceExample → tearDown
Tip: Pengujian dijalankan dengan menekan cmd + U, dengan memilih Produk → Uji, atau dengan mengklik dan menahan tombol Jalankan hingga menu opsi muncul, lalu pilih Uji dari menu.
Jika Anda ingin menjalankan hanya satu metode pengujian tertentu, tekan tombol di sebelah kiri nama metode (ditunjukkan pada gambar di bawah).
Sekarang, ketika Anda memiliki segalanya untuk menulis tes, Anda dapat menambahkan kelas contoh dan beberapa metode untuk diuji.
Tambahkan kelas yang akan bertanggung jawab untuk pendaftaran pengguna. Seorang pengguna memasukkan alamat email, kata sandi, dan konfirmasi kata sandi. Kelas contoh kami akan memvalidasi input, memeriksa ketersediaan alamat email, dan mencoba pendaftaran pengguna.
Catatan: contoh ini menggunakan pola arsitektur MVVM (atau Model-View-ViewModel).
MVVM digunakan karena membuat arsitektur aplikasi lebih bersih dan lebih mudah untuk diuji.
Dengan MVVM, lebih mudah untuk memisahkan logika bisnis dari logika presentasi, sehingga menghindari masalah pengontrol tampilan yang masif.
Detail tentang arsitektur MVVM berada di luar cakupan artikel ini, tetapi Anda dapat membacanya lebih lanjut di artikel ini.
Mari buat kelas model tampilan yang bertanggung jawab untuk pendaftaran pengguna. .
class RegisterationViewModel { var emailAddress: String? { didSet { enableRegistrationAttempt() } } var password: String? { didSet { enableRegistrationAttempt() } } var passwordConfirmation: String? { didSet { enableRegistrationAttempt() } } var registrationEnabled = Dynamic(false) var errorMessage = Dynamic("") var loginSuccessful = Dynamic(false) var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
Pertama, kami telah menambahkan beberapa properti, properti dinamis, dan metode init.
Jangan khawatir tentang tipe Dynamic
. Itu bagian dari arsitektur MVVM.
Saat nilai Dynamic<Bool>
disetel ke true, pengontrol tampilan yang terikat (terhubung) ke RegistrationViewModel
akan mengaktifkan tombol registrasi. Ketika loginSuccessful
disetel ke true, tampilan yang terhubung akan diperbarui dengan sendirinya.
Sekarang mari tambahkan beberapa metode untuk memeriksa validitas kata sandi dan format email.
func enableRegistrationAttempt() { registrationEnabled.value = emailValid() && passwordValid() } func emailValid() -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailTest.evaluate(with: emailAddress) } func passwordValid() -> Bool { guard let password = password, let passwordConfirmation = passwordConfirmation else { return false } let isValid = (password == passwordConfirmation) && password.characters.count >= 6 return isValid }
Setiap kali pengguna mengetik sesuatu di bidang email atau kata sandi, metode enableRegistrationAttempt
akan memeriksa apakah email dan kata sandi dalam format yang benar dan mengaktifkan atau menonaktifkan tombol pendaftaran melalui properti dinamis registrationEnabled
.
Untuk menjaga agar contoh tetap sederhana, tambahkan dua metode sederhana – satu untuk memeriksa ketersediaan email dan satu lagi untuk mencoba pendaftaran dengan nama pengguna dan kata sandi yang diberikan.
func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) { networkService.checkEmailAvailability(email: email) { (available, error) in if let _ = error { self.errorMessage.value = "Our custom error message" } else if !available { self.errorMessage.value = "Sorry, provided email address is already taken" self.registrationEnabled.value = false callback(available) } } } func attemptUserRegistration() { guard registrationEnabled.value == true else { return } // To keep the example as simple as possible, password won't be hashed guard let emailAddress = emailAddress, let passwordHash = password else { return } networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) { (success, error) in // Handle the response if let _ = error { self.errorMessage.value = "Our custom error message" } else { self.loginSuccessful.value = true } } }
Kedua metode ini menggunakan NetworkService untuk memeriksa apakah email tersedia dan untuk mencoba pendaftaran.
Untuk menjaga agar contoh ini tetap sederhana, implementasi NetworkService tidak menggunakan API back-end apa pun, tetapi hanya sebuah rintisan yang memalsukan hasilnya. NetworkService diimplementasikan sebagai protokol dan kelas implementasinya.
typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void protocol NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) }
NetworkService adalah protokol yang sangat sederhana yang hanya berisi dua metode: upaya pendaftaran dan metode pemeriksaan ketersediaan email. Implementasi protokol adalah kelas NetworkServiceImpl.
class NetworkServiceImpl: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } }
Kedua metode hanya menunggu beberapa waktu (memalsukan waktu tunda permintaan jaringan) dan kemudian memanggil metode panggilan balik yang sesuai.
Tip: Ini adalah praktik yang baik untuk menggunakan protokol (juga dikenal sebagai antarmuka dalam bahasa pemrograman lain). Anda dapat membaca lebih lanjut tentangnya jika Anda mencari 'prinsip pemrograman ke antarmuka'. Anda juga akan melihat cara kerjanya dengan baik dengan pengujian unit.
Sekarang, ketika sebuah contoh ditetapkan, kita dapat menulis pengujian unit untuk mencakup metode kelas ini.
Buat kelas pengujian baru untuk model tampilan kami. Klik kanan pada folder
TestingIOSTests
di panel Project Navigator, pilih New File → Unit Test Case Class, dan beri namaRegistrationViewModelTests
.Hapus metode
testExample
dantestPerformanceExample
, karena kami ingin membuat metode pengujian kami sendiri.Karena Swift menggunakan modul dan pengujian kami berada dalam modul yang berbeda dari kode aplikasi kami, kami harus mengimpor modul aplikasi kami sebagai
@testable
. Di bawah pernyataan impor dan definisi kelas, tambahkan@testable import TestingIOS
(atau nama modul aplikasi Anda). Tanpa ini, kami tidak akan dapat mereferensikan kelas atau metode aplikasi kami.Tambahkan variabel
registrationViewModel
.
Beginilah tampilan kelas pengujian kosong kami sekarang:
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
Mari kita coba menulis tes untuk metode emailValid
. Kami akan membuat metode pengujian baru yang disebut testEmailValid
. Penting untuk menambahkan kata kunci test
di awal nama. Jika tidak, metode tersebut tidak akan dikenali sebagai metode pengujian.
Metode pengujian kami terlihat seperti ini:
func testEmailValid() { let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl()) registrationVM.emailAddress = "email.test.com" XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct") registrationVM.emailAddress = "email@test" XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct") registrationVM.emailAddress = nil XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct") registrationVM.emailAddress = "[email protected]" XCTAssert(registrationVM.emailValid(), "\(registrationVM.emailAddress) should be correct") }
Metode pengujian kami menggunakan metode pernyataan, XCTAssert
, yang dalam kasus kami memeriksa apakah suatu kondisi benar atau salah.
Jika kondisinya salah, pernyataan akan gagal (bersama dengan tes), dan pesan kami akan ditulis.
Ada banyak metode penegasan yang dapat Anda gunakan dalam pengujian Anda. Menggambarkan dan menunjukkan setiap metode penegasan dapat dengan mudah membuat artikelnya sendiri, jadi saya tidak akan membahas detailnya di sini.
Beberapa contoh metode penegasan yang tersedia adalah: XCTAssertEqualObjects
, XCTAssertGreaterThan
, XCTAssertNil
, XCTAssertTrue
atau XCTAssertThrows
.
Anda dapat membaca lebih lanjut tentang metode penegasan yang tersedia di sini.
Jika Anda menjalankan pengujian sekarang, metode pengujian akan lulus. Anda telah berhasil membuat metode pengujian pertama, tetapi metode tersebut belum siap untuk prime time. Metode pengujian ini masih memiliki tiga masalah (satu besar dan dua lebih kecil), seperti yang dijelaskan di bawah ini.
Masalah 1: Anda menggunakan implementasi nyata dari protokol NetworkService
Salah satu prinsip inti pengujian unit adalah bahwa setiap pengujian harus independen dari faktor atau ketergantungan luar. Tes unit harus atom.
Jika Anda menguji metode, yang pada titik tertentu memanggil metode API dari server, pengujian Anda memiliki ketergantungan pada kode jaringan Anda dan pada ketersediaan server. Jika server tidak berfungsi pada saat pengujian, pengujian Anda akan gagal, sehingga salah menuduh metode pengujian Anda tidak berfungsi.
Dalam hal ini, Anda menguji metode RegistrationViewModel
.
RegistrationViewModel
bergantung pada kelas NetworkServiceImpl
, meskipun Anda tahu bahwa metode yang Anda uji, emailValid
, tidak bergantung pada NetworkServiceImpl
secara langsung.
Saat menulis tes unit, semua dependensi luar harus dihapus. Tetapi bagaimana Anda harus menghapus ketergantungan NetworkService tanpa mengubah implementasi kelas RegistrationViewModel
?
Ada solusi mudah untuk masalah ini, dan itu disebut Object Mocking . Jika Anda melihat lebih dekat pada RegistrationViewModel
, Anda akan melihat bahwa itu sebenarnya bergantung pada protokol NetworkService
.
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
Saat RegistrationViewModel
sedang diinisialisasi, implementasi protokol NetworkService
diberikan (atau disuntikkan) ke objek RegistrationViewModel
.
Prinsip ini disebut injeksi ketergantungan melalui konstruktor ( ada lebih banyak jenis injeksi ketergantungan ).
Ada banyak artikel menarik tentang injeksi ketergantungan online, seperti artikel ini di objc.io.
Ada juga artikel singkat namun menarik yang menjelaskan tentang injeksi ketergantungan dengan cara yang sederhana dan lugas di sini.
Selain itu, artikel bagus tentang prinsip tanggung jawab tunggal dan DI tersedia di blog Toptal.
Ketika RegistrationViewModel
dipakai, itu menyuntikkan implementasi protokol NetworkService di konstruktornya (karenanya nama prinsip injeksi ketergantungan):
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
Karena kelas model tampilan kami hanya bergantung pada protokol, tidak ada yang menghentikan kami untuk membuat kelas implementasi NetworkService
kustom (atau tiruan) kami dan menyuntikkan kelas tiruan ke objek model tampilan kami.
Mari kita buat implementasi protokol NetworkService
tiruan kita.
Tambahkan file Swift baru ke target pengujian kami dengan mengklik kanan pada folder TestingIOSTests
di Project Navigator, pilih "File Baru", pilih "file Swift", dan beri nama NetworkServiceMock
.
Beginilah tampilan kelas tiruan kita:
import Foundation @testable import TestingIOS class NetworkServiceMock: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(false, nil) }) } }
Pada titik ini, ini tidak jauh berbeda dari implementasi aktual kami ( NetworkServiceImpl
), tetapi dalam situasi dunia nyata, NetworkServiceImpl
yang sebenarnya akan memiliki kode jaringan, penanganan respons, dan fungsionalitas serupa.
Kelas tiruan kami tidak melakukan apa-apa, yang merupakan inti dari kelas yang diolok-olok. Jika tidak melakukan apa-apa daripada itu tidak akan mengganggu pengujian kami.
Untuk memperbaiki masalah pertama pengujian kami, mari perbarui metode pengujian kami dengan mengganti:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
dengan:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
Masalah 2: Anda membuat instance RegistrationVM di badan metode pengujian
Ada metode setUp
dan tearDown
karena suatu alasan.
Metode tersebut digunakan untuk memulai atau menyiapkan semua objek yang diperlukan dalam pengujian. Anda harus menggunakan metode tersebut untuk menghindari duplikasi kode dengan menulis metode init atau penyiapan yang sama di setiap metode pengujian. Tidak menggunakan metode penyiapan dan tearDown tidak selalu menjadi masalah besar, terutama jika Anda memiliki konfigurasi yang sangat spesifik untuk metode pengujian tertentu.
Karena inisialisasi kelas RegistrationViewModel
kami cukup sederhana, Anda akan memfaktorkan ulang kelas pengujian Anda untuk menggunakan metode setup dan tearDown.
RegistrationViewModelTests
akan terlihat seperti ini:
class RegistrationViewModelTests: XCTestCase { var registrationVM: RegisterationViewModel! override func setUp() { super.setUp() registrationVM = RegisterationViewModel(networkService: NetworkServiceMock()) } override func tearDown() { registrationVM = nil super.tearDown() } func testEmailValid() { registrationVM.emailAddress = "email.test.com" XCTAssertFalse(registrationVM.emailValid(), "\(registrationVM.emailAddress) shouldn't be correct") ... } }
Masalah 3: Anda memiliki beberapa pernyataan dalam satu metode pengujian
Meskipun ini bukan masalah besar, ada beberapa pendukung untuk memiliki satu pernyataan per metode.
Alasan utama untuk prinsip ini adalah deteksi kesalahan.
Jika satu metode pengujian memiliki beberapa pernyataan dan yang pertama gagal, seluruh metode pengujian akan ditandai sebagai gagal. Pernyataan lain bahkan tidak akan diuji.

Dengan cara ini Anda hanya akan menemukan satu kesalahan dalam satu waktu. Anda tidak akan tahu apakah pernyataan lain akan gagal atau berhasil.
Tidak selalu buruk untuk memiliki banyak pernyataan dalam satu metode karena Anda hanya dapat memperbaiki satu kesalahan dalam satu waktu, jadi mendeteksi satu kesalahan dalam satu waktu mungkin bukan masalah besar.
Dalam kasus kami, validitas format email diuji. Karena ini hanya satu fungsi, mungkin lebih logis untuk mengelompokkan semua pernyataan bersama dalam satu metode untuk membuat tes lebih mudah dibaca dan dipahami.
Karena masalah ini sebenarnya bukan masalah besar dan beberapa orang bahkan mungkin berpendapat bahwa itu bukan masalah sama sekali, Anda akan mempertahankan metode pengujian apa adanya.
Saat Anda menulis pengujian unit Anda sendiri, terserah Anda untuk memutuskan jalur mana yang ingin Anda ambil untuk setiap metode pengujian. Kemungkinan besar, Anda akan menemukan bahwa ada tempat di mana satu pernyataan filosofi per tes masuk akal, dan yang lain tidak.
Metode Pengujian dengan Panggilan Asinkron
Tidak peduli seberapa sederhana aplikasinya, ada kemungkinan besar akan ada metode yang perlu dieksekusi di utas lain secara asinkron, terutama karena Anda biasanya ingin UI dieksekusi di utasnya sendiri..
Masalah utama dengan pengujian unit dan panggilan asinkron adalah bahwa panggilan asinkron membutuhkan waktu untuk diselesaikan, tetapi pengujian unit tidak akan menunggu sampai selesai. Karena pengujian unit selesai sebelum kode apa pun di dalam blok asinkron dijalankan, pengujian kami akan selalu berakhir dengan hasil yang sama (apa pun yang Anda tulis di blok asinkron).
Untuk mendemonstrasikan ini, mari buat pengujian untuk metode checkEmailAvailability
.
func testCheckEmailAvailability() { registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: "[email protected]") { available in XCTAssert(self.registrationVM.registrationEnabled.value == false, "Email address is not available, registration should be disabled") } }
Di sini Anda ingin menguji apakah variabel RegistrationEnabled akan disetel ke false setelah metode kami memberi tahu Anda bahwa email tidak tersedia (sudah diambil oleh pengguna lain).
Jika Anda menjalankan tes ini, itu akan lulus. Tapi coba satu hal lagi. Ubah pernyataan Anda menjadi:
XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")
Jika Anda menjalankan tes lagi, itu lolos lagi.
Ini karena pernyataan kami bahkan tidak ditegaskan. Tes unit berakhir sebelum blok panggilan balik dieksekusi (ingat, dalam implementasi layanan jaringan tiruan kami, ini diatur untuk menunggu satu detik sebelum kembali).
Untungnya, dengan Xcode 6, Apple telah menambahkan ekspektasi pengujian ke kerangka XCTest sebagai kelas XCTestExpectation
. Kelas XCTestExpectation
bekerja seperti ini:
- Di awal pengujian, Anda menetapkan ekspektasi pengujian - dengan teks sederhana yang menjelaskan apa yang Anda harapkan dari pengujian.
- Di blok async setelah kode pengujian Anda dieksekusi, Anda kemudian memenuhi harapan.
- Di akhir pengujian, Anda perlu menyetel blok
waitForExpectationWithTimer
. Ini akan dieksekusi ketika harapan terpenuhi atau jika penghitung waktu habis - mana yang lebih dulu. - Sekarang, unit test tidak akan selesai sampai ekspektasi terpenuhi atau sampai timer ekspektasi habis.
Mari kita tulis ulang pengujian kita untuk menggunakan kelas XCTestExpectation
.
func testCheckEmailAvailability() { // 1. Setting the expectation let exp = expectation(description: "Check email availability") registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: "[email protected]") { available in XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled") // 2. Fulfilling the expectation exp.fulfill() } // 3. Waiting for expectation to fulfill waitForExpectations(timeout: 3.0) { error in if let _ = error { XCTAssert(false, "Timeout while checking email availability") } } }
Jika Anda menjalankan tes sekarang, itu akan gagal - sebagaimana mestinya. Mari kita perbaiki tes untuk membuatnya lulus. Ubah pernyataan menjadi:
XCTAssert(self.registrationVM.registrationEnabled.value == false, "Email address is not available, registration should be disabled")
Jalankan tes lagi untuk melihatnya lulus. Anda dapat mencoba mengubah waktu tunda dalam implementasi tiruan layanan jaringan untuk melihat apa yang terjadi jika penghitung waktu ekspektasi habis.
Menguji Metode dengan Panggilan Asinkron tanpa Panggilan Balik
Contoh metode proyek kami, attemptUserRegistration
menggunakan metode NetworkService.attemptRegistration
yang menyertakan kode yang dijalankan secara asinkron. Metode ini mencoba mendaftarkan pengguna dengan layanan backend.
Dalam aplikasi demo kami, metode ini hanya akan menunggu satu detik untuk mensimulasikan panggilan jaringan, dan memalsukan pendaftaran yang berhasil. Jika pendaftaran berhasil, nilai loginSuccessful
akan disetel ke true. Mari kita buat unit test untuk memverifikasi perilaku ini.
func testAttemptRegistration() { registrationVM.emailAddress = "[email protected]" registrationVM.password = "123456" registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, "Login must be successful") }
Jika dijalankan, pengujian ini akan gagal karena nilai loginSuccessful
tidak akan disetel ke true hingga metode asynchronous networkService.attemptRegistration
selesai.
Karena Anda telah membuat NetworkServiceImpl
tiruan di mana metode attemptRegistration
akan menunggu satu detik sebelum mengembalikan pendaftaran yang berhasil, Anda bisa menggunakan Grand Central Dispatch (GCD), dan menggunakan metode asyncAfter
untuk memeriksa pernyataan Anda setelah satu detik. Setelah menambahkan asyncAfter
GCD, kode pengujian kami akan terlihat seperti ini:
func testAttemptRegistration() { registrationVM.emailAddress = "[email protected]" registrationVM.password = "123456" registrationVM.passwordConfirmation = "123456" registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, "Login must be successful") } }
Jika Anda memperhatikan, Anda akan tahu bahwa ini masih tidak akan berhasil karena metode pengujian akan dijalankan sebelum blok asyncAfter
dijalankan dan sebagai hasilnya metode akan selalu berhasil lulus. Untungnya, ada kelas XCTestException
.
Mari kita tulis ulang metode kita untuk menggunakan kelas XCTestException
:
func testAttemptRegistration() { let exp = expectation(description: "Check registration attempt") registrationVM.emailAddress = "[email protected]" registrationVM.password = "123456" registrationVM.passwordConfirmation = "123456" registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, "Login must be successful") exp.fulfill() } waitForExpectations(timeout: 4.0) { error in if let _ = error { XCTAssert(false, "Timeout while attempting a registration") } } }
Dengan pengujian unit yang mencakup RegistrationViewModel
kami, kini Anda dapat lebih yakin bahwa menambahkan fungsionalitas baru, atau memperbarui yang sudah ada, tidak akan merusak apa pun.
Catatan Penting: Pengujian unit akan kehilangan nilainya jika tidak diperbarui saat fungsionalitas metode yang dicakupnya berubah. Tes unit menulis adalah proses yang harus mengikuti sisa aplikasi.
Tip: Jangan tunda tes menulis sampai akhir. Menulis tes saat berkembang. Dengan cara ini Anda akan memiliki pemahaman yang lebih baik tentang apa yang perlu diuji dan apa saja kasus perbatasan.
Menulis Tes UI
Setelah semua pengujian unit sepenuhnya dikembangkan dan dijalankan dengan sukses, Anda bisa sangat yakin bahwa setiap unit kode berfungsi dengan benar, tetapi apakah itu berarti aplikasi Anda secara keseluruhan berfungsi sebagaimana mestinya?
Di situlah tes integrasi masuk, di mana tes UI merupakan komponen penting.
Sebelum memulai pengujian UI, perlu ada beberapa elemen dan interaksi UI (atau cerita pengguna) untuk diuji. Mari kita buat tampilan sederhana dan pengontrol tampilannya.
- Buka
Main.storyboard
dan buat pengontrol tampilan sederhana yang akan terlihat seperti pada gambar di bawah.
Setel tag bidang teks email ke 100, tag bidang teks kata sandi ke 101, dan tag konfirmasi kata sandi ke 102.
- Tambahkan file pengontrol tampilan baru
RegistrationViewController.swift
dan hubungkan semua outlet dengan storyboard.
import UIKit class RegistrationViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordConfirmationTextField: UITextField! @IBOutlet weak var registerButton: UIButton! private struct TextFieldTags { static let emailTextField = 100 static let passwordTextField = 101 static let confirmPasswordTextField = 102 } var viewModel: RegisterationViewModel? override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self passwordConfirmationTextField.delegate = self bindViewModel() } }
Di sini Anda menambahkan IBOutlets
dan struct TextFieldTags
ke kelas.
Ini akan memungkinkan Anda untuk mengidentifikasi bidang teks mana yang sedang diedit. Untuk memanfaatkan properti Dinamis dalam model tampilan, Anda harus 'mengikat' properti dinamis di pengontrol tampilan. Anda dapat melakukannya dalam metode bindViewModel
:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = $0 } } }
Sekarang mari tambahkan metode delegasi bidang teks untuk melacak saat salah satu bidang teks diperbarui:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let viewModel = viewModel else { return true } let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) switch textField.tag { case TextFieldTags.emailTextField: viewModel.emailAddress = newString case TextFieldTags.passwordTextField: viewModel.password = newString case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString default: break } return true }
- Perbarui
AppDelegate
untuk mengikat pengontrol tampilan ke model tampilan yang sesuai (perhatikan bahwa langkah ini adalah persyaratan arsitektur MVVM). KodeAppDelegate
yang diperbarui akan terlihat seperti ini:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { initializeStartingView() return true } fileprivate func initializeStartingView() { if let rootViewController = window?.rootViewController as? RegistrationViewController { let networkService = NetworkServiceImpl() let viewModel = RegisterationViewModel(networkService: networkService) rootViewController.viewModel = viewModel } }
File storyboard dan RegistrationViewController
sangat sederhana, tetapi cukup untuk menunjukkan cara kerja pengujian UI otomatis.
Jika semuanya diatur dengan benar, tombol pendaftaran harus dinonaktifkan saat aplikasi dimulai. Ketika, dan hanya jika, semua bidang diisi dan valid, tombol registrasi harus diaktifkan.
Setelah ini diatur, Anda dapat membuat pengujian UI pertama Anda.
Pengujian UI kami harus memeriksa apakah tombol Daftar akan diaktifkan jika dan hanya jika alamat email yang valid, kata sandi yang valid, dan konfirmasi kata sandi yang valid semuanya telah dimasukkan. Berikut cara menyiapkannya:
- Buka file
TestingIOSUITests.swift
. - Hapus metode
testExample()
dan tambahkan metodetestRegistrationButtonEnabled()
. - Letakkan kursor di metode
testRegistrationButtonEnabled
seperti Anda akan menulis sesuatu di sana. - Tekan tombol uji Rekam UI (lingkaran merah di bagian bawah layar).
- Ketika tombol Rekam ditekan, aplikasi akan diluncurkan
- Setelah aplikasi diluncurkan, ketuk di bidang teks email dan tulis '[email protected]'. Anda akan melihat bahwa kode tersebut secara otomatis muncul di dalam badan metode pengujian.
Anda dapat merekam semua instruksi UI menggunakan fitur ini, tetapi Anda mungkin menemukan bahwa menulis instruksi sederhana secara manual akan jauh lebih cepat.
Ini adalah contoh instruksi perekam untuk mengetuk bidang teks kata sandi dan memasukkan alamat email '[email protected]'
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]")
- Setelah interaksi UI yang ingin Anda uji direkam, tekan tombol stop lagi (label tombol rekam diubah menjadi berhenti saat Anda mulai merekam) untuk menghentikan perekaman.
- Setelah Anda memiliki perekam interaksi UI, Anda sekarang dapat menambahkan berbagai
XCTAsserts
untuk menguji berbagai status aplikasi atau elemen UI.
Instruksi yang direkam tidak selalu cukup jelas dan bahkan mungkin membuat keseluruhan metode pengujian agak sulit untuk dibaca dan dipahami. Untungnya, Anda dapat memasukkan instruksi UI secara manual.
Let's create the following UI instructions manually:
- User taps on the password text field.
- User enters a 'password'.
To reference a UI element, you can use a placeholder identifier. A placeholder identifier can be set in the storyboard in the Identity Inspector pane under Accessibility. Set the password text field's accessibility identifier to 'passwordTextField'.
The password UI interaction can now be written as:
let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"] passwordTextField.tap() passwordTextField.typeText("password")
There is one more UI interaction left: the confirm password input interaction. This time, you'll reference the confirm password text field by its placeholder. Go to storyboard and add the 'Confirm Password' placeholder for the confirm password text field. The user interaction can now be written like this:
let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"] confirmPasswordTextField.tap() confirmPasswordTextField.typeText("password")
Now, when you have all required UI interactions, all that is left is to write a simple XCTAssert
(the same as you did in unit testing) to verify if the Register button's isEnabled
state is set to true. The register button can be referenced using its title. Assert to check a button's isEnabled
property looks like this:
let registerButton = XCUIApplication().buttons["REGISTER"] XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled")
The whole UI test should now look like this:
func testRegistrationButtonEnabled() { // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]") // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"] passwordTextField.tap() passwordTextField.typeText("password") // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"] confirmPasswordTextField.tap() confirmPasswordTextField.typeText("password") let registerButton = XCUIApplication().buttons["REGISTER"] XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled") }
If the test is run, Xcode will start the simulator and launch our test application. After the application is launched, our UI interaction instructions will be run one by one and at the end the assert will be successfully asserted.
To improve the test, let's also test that the isEnabled
property of the register button is false whenever any of the required fields have not been not entered correctly.
The complete test method should now look like this:
func testRegistrationButtonEnabled() { let registerButton = XCUIApplication().buttons["REGISTER"] XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled") // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]") XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled") // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields["passwordTextField"] passwordTextField.tap() passwordTextField.typeText("password") XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled") // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields["Confirm Password"] confirmPasswordTextField.tap() confirmPasswordTextField.typeText("pass") XCTAssert(registerButton.isEnabled == false, "Registration button should be disabled") confirmPasswordTextField.typeText("word") // the whole confirm password word will now be "password" XCTAssert(registerButton.isEnabled == true, "Registration button should be enabled") }
Tip: The preferred way to identify UI elements is by using accessibility identifiers. If names, placeholders, or some other property that can be localized is used, the element won't be found if a different language is used in which case the test would fail.
The example UI test is very simple, but it demonstrates the power of automated UI testing.
The best way to discover all possibilities (and there are many) of the UI testing framework included in Xcode is to start writing UI tests in your projects. Start with simple user stories, like the one shown, and slowly move to more complex stories and tests.
Become a Better Developer by Writing Good Tests
From my experience, learning and trying to write good tests will make you think about other aspects of development. It will help you become a better iOS developer altogether.
To write good tests, you will have to learn how to better organize your code.
Organized, modular, well-written code is the main requirement for successful and stress-free unit and UI testing.
In some cases, it is even impossible to write tests when code is not organized well.
When thinking about application structure and code organization, you'll realize that by using MVVM, MVP, VIPER, or other such patterns, your code will be better structured, modular, and easy to test (you will also avoid Massive View Controller issues).
When writing tests, you will undoubtedly, at some point, have to create a mocked class. It will make you think and learn about the dependency injection principle and protocol-oriented coding practices. Knowing and using those principles will notably increase your future projects' code quality.
Once you begin writing tests, you will probably notice yourself thinking more about corner cases and edge conditions as you write your code. This will help you eliminate possible bugs before they become bugs. Thinking about possible issues and negative outcomes of methods, you won't only test positive outcomes, but you will also start to test negative outcomes too.
As you can see, unit tests can have impact on different development aspects, and by writing good unit and UI tests, you will likely become a better and happier developer (and you won't have to spend as much time fixing bugs).
Start writing automated tests, and eventually you'll see the benefits of automated testing. When you see it for yourself, you'll become its strongest advocate.