iOS için Otomatik Testler Nasıl Yazılır
Yayınlanan: 2022-03-11İyi bir geliştirici olarak, yazdığınız yazılımdaki tüm işlevleri ve olası her kod yolunu ve sonucunu test etmek için elinizden gelenin en iyisini yaparsınız. Ancak, bir kullanıcının alabileceği her olası sonucu ve olası her yolu manuel olarak test edebilmek son derece nadir ve olağandışıdır.
Uygulama büyüdükçe ve karmaşıklaştıkça, manuel test yoluyla bir şeyleri gözden kaçırma olasılığınız önemli ölçüde artar.
Hem UI hem de arka uç hizmet API'lerinin otomatik olarak test edilmesi, her şeyin amaçlandığı gibi çalıştığından emin olmanızı sağlayacak ve geliştirme, yeniden düzenleme, yeni özellikler ekleme veya mevcut özellikleri değiştirme sırasındaki stresi azaltacaktır.
Otomatik testlerle şunları yapabilirsiniz:
- Hataları azaltın: Kodunuzdaki hata olasılığını tamamen ortadan kaldıracak bir yöntem yoktur, ancak otomatik testler hata sayısını büyük ölçüde azaltabilir.
- Değişiklikleri güvenle yapın: Yeni özellikler eklerken hatalardan kaçının, bu da değişiklikleri hızlı ve zahmetsizce yapabileceğiniz anlamına gelir.
- Kodumuzu belgeleyin: Testlere bakarken belirli işlevlerden ne beklendiğini, koşulların neler olduğunu ve köşe durumlarının neler olduğunu açıkça görebiliriz.
- Acısız bir şekilde yeniden düzenleme: Bir geliştirici olarak, özellikle büyük bir kod yığınını yeniden düzenlemeniz gerekiyorsa, bazen yeniden düzenleme yapmaktan korkabilirsiniz. Birim testleri, yeniden düzenlenmiş kodun hala istendiği gibi çalıştığından emin olmak için burada.
Bu makale, iOS platformunda otomatik testlerin nasıl yapılandırılacağını ve yürütüleceğini öğretir.
Birim Testleri ve UI Testleri
Birim ve UI testleri arasında ayrım yapmak önemlidir.
Birim testi , belirli bir bağlam altında belirli bir işlevi test eder. Birim testleri, kodun test edilen bölümünün (genellikle tek bir işlev) yapması gerekeni yaptığını doğrular. Birim testleri hakkında birçok kitap ve makale var, bu yüzden bu yazıda bunu ele almayacağız.
UI testleri , kullanıcı arayüzünü test etmek içindir. Örneğin, bir görünümün istendiği gibi güncellenip güncellenmediğini veya kullanıcı belirli bir UI öğesiyle etkileşime girdiğinde olması gerektiği gibi belirli bir eylemin tetiklenip tetiklenmediğini test etmenize olanak tanır.
Her UI testi, uygulamanın UI'si ile belirli bir kullanıcı etkileşimini test eder. Otomatik test, hem birim testi hem de UI testi seviyelerinde yapılabilir ve yapılmalıdır.
Otomatik Testleri Ayarlama
XCode kutudan çıktığı gibi birim ve UI testini desteklediğinden, bunları projenize eklemek kolay ve basittir. Yeni bir proje oluştururken, "Birim Testlerini Dahil Et" ve "Kullanıcı Arayüzü Testlerini Dahil Et"i işaretlemeniz yeterlidir.
Proje oluşturulduğunda bu iki seçenek işaretlendiğinde projenize iki yeni hedef eklenecektir. Yeni hedef adlarının sonuna “Testler” veya “UITestler” eklenir.
Bu kadar. Projeniz için otomatik testler yazmaya hazırsınız.
Halihazırda mevcut bir projeniz varsa ve kullanıcı arayüzü ve Birim testleri desteğini eklemek istiyorsanız, biraz daha çalışmanız gerekecek, ancak bu aynı zamanda çok basit ve basit.
Dosya → Yeni → Hedef'e gidin ve Birim testleri için iOS Birim Test Paketi'ni veya UI testleri için iOS UI Test Paketi'ni seçin.
İleri'ye basın.
Hedef seçenekleri ekranında, her şeyi olduğu gibi bırakabilirsiniz (birden fazla hedefiniz varsa ve yalnızca belirli hedefleri test etmek istiyorsanız, Test edilecek hedef açılır menüsünden hedefi seçin).
Bitir'e basın. UI testleri için bu adımı tekrarlayın ve mevcut projenizde otomatik testler yazmaya başlamak için her şeye hazır olacaksınız.
Birim Testleri Yazma
Birim testleri yazmaya başlamadan önce anatomilerini anlamalıyız. Projenize birim testleri eklediğinizde örnek bir test sınıfı oluşturulacaktır. Bizim durumumuzda, şöyle görünecek:
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. } } }
Anlaşılması gereken en önemli yöntemler setUp
ve yırtma tearDown
. Her test yönteminden önce setUp
yöntemi çağrılırken, her test yönteminden sonra tearDown
yöntemi çağrılır. Bu örnek test sınıfında tanımlanan testleri çalıştırırsak, yöntemler şu şekilde çalışır:
kurulum → testÖrneği → yırtma kurulumu → testPerformanceÖrneği → gözyaşıDown
İpucu: Testler, cmd + U tuşlarına basılarak, Ürün → Test seçilerek veya seçenekler menüsü görünene kadar Çalıştır düğmesini tıklayıp basılı tutarak, ardından menüden Test'i seçerek çalıştırılır.
Yalnızca belirli bir test yöntemini çalıştırmak istiyorsanız, yöntemin adının solundaki düğmeye basın (aşağıdaki resimde gösterilmiştir).
Şimdi, test yazmak için her şey hazır olduğunda, test etmek için örnek bir sınıf ve bazı yöntemler ekleyebilirsiniz.
Kullanıcı kaydından sorumlu olacak bir sınıf ekleyin. Bir kullanıcı bir e-posta adresi, şifre ve şifre onayı girer. Örnek sınıfımız girişi doğrulayacak, e-posta adresinin uygunluğunu kontrol edecek ve kullanıcı kaydı yapmaya çalışacaktır.
Not: Bu örnek, MVVM (veya Model-View-ViewModel) mimari modelini kullanmaktadır.
MVVM, bir uygulamanın mimarisini daha temiz ve test etmeyi kolaylaştırdığı için kullanılır.
MVVM ile, iş mantığını sunum mantığından ayırmak daha kolaydır, böylece büyük görünüm denetleyicisi sorunundan kaçınılır.
MVVM mimarisi ile ilgili ayrıntılar bu makalenin kapsamı dışındadır, ancak bu makale hakkında daha fazla bilgi edinebilirsiniz.
Kullanıcı kaydından sorumlu bir görünüm modeli sınıfı oluşturalım. .
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 } }
İlk olarak, birkaç özellik, dinamik özellik ve bir init yöntemi ekledik.
Dynamic
tür hakkında endişelenmeyin. MVVM mimarisinin bir parçasıdır.
Bir Dynamic<Bool>
değeri true olarak ayarlandığında, RegistrationViewModel
bağlı (bağlı) bir görünüm denetleyicisi kayıt düğmesini etkinleştirir. loginSuccessful
true olarak ayarlandığında, bağlı görünüm kendini güncelleyecektir.
Şimdi şifre ve e-posta formatının geçerliliğini kontrol etmek için birkaç yöntem ekleyelim.
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 }
Kullanıcı e-posta veya şifre alanına her bir şey yazdığında, enableRegistrationAttempt
yöntemi, bir e-posta ve şifrenin doğru biçimde olup olmadığını kontrol edecek ve kayıt Etkinleştirildi dinamik özelliği aracılığıyla registrationEnabled
düğmesini etkinleştirecek veya devre dışı bırakacaktır.
Örneği basit tutmak için, iki basit yöntem ekleyin - biri e-posta kullanılabilirliğini kontrol etmek için, diğeri de verilen kullanıcı adı ve şifreyle kayıt girişiminde bulunmak.
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 } } }
Bu iki yöntem, bir e-postanın mevcut olup olmadığını kontrol etmek ve kayıt girişiminde bulunmak için NetworkService'i kullanır.
Bu örneği basit tutmak için, NetworkService uygulaması herhangi bir arka uç API kullanmaz, yalnızca sonuçları taklit eden bir saplamadır. NetworkService, bir protokol ve uygulama sınıfı olarak uygulanır.
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, yalnızca iki yöntem içeren çok basit bir protokoldür: kayıt girişimi ve e-posta kullanılabilirliği kontrol yöntemleri. Protokol uygulaması NetworkServiceImpl sınıfıdır.
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) }) } }
Her iki yöntem de sadece bir süre bekleyin (bir ağ isteğinin gecikme süresini taklit ederek) ve ardından uygun geri arama yöntemlerini çağırın.
İpucu: Protokolleri (diğer programlama dillerinde arabirimler olarak da bilinir) kullanmak iyi bir uygulamadır. 'Arayüzlere programlama ilkesi' için arama yaparsanız, bununla ilgili daha fazla bilgi edinebilirsiniz. Ayrıca birim testiyle nasıl iyi çalıştığını da göreceksiniz.
Şimdi, bir örnek oluşturulduğunda, bu sınıfın yöntemlerini kapsayacak şekilde birim testleri yazabiliriz.
Görünüm modelimiz için yeni bir test sınıfı oluşturun. Proje Gezgini bölmesindeki
TestingIOSTests
klasörüne sağ tıklayın, New File → Unit Test Case Class'ı seçin ve adınıRegistrationViewModelTests
olarak adlandırın.Kendi test yöntemlerimizi oluşturmak istediğimiz için
testExample
vetestPerformanceExample
yöntemlerini silin.Swift modülleri kullandığından ve testlerimiz uygulamamızın kodundan farklı bir modülde olduğundan, uygulamamızın modülünü
@testable
olarak almamız gerekiyor. import ifadesinin ve sınıf tanımının@testable import TestingIOS
(veya uygulamanızın modül adını) ekleyin. Bu olmadan, uygulamamızın sınıflarına veya yöntemlerine referans veremezdik.registrationViewModel
değişkenini ekleyin.
Boş test sınıfımız şimdi böyle görünüyor:
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
emailValid
metodu için bir test yazmaya çalışalım. testEmailValid
adında yeni bir test yöntemi oluşturacağız. test
anahtar sözcüğünü adın başına eklemek önemlidir. Aksi takdirde, yöntem bir test yöntemi olarak tanınmayacaktır.
Test yöntemimiz şöyle görünür:
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") }
Test yöntemimiz, bizim durumumuzda bir koşulun doğru veya yanlış olup olmadığını kontrol eden bir onaylama yöntemi olan XCTAssert
kullanır.
Koşul yanlışsa, assert başarısız olur (testle birlikte) ve mesajımız yazılır.
Testlerinizde kullanabileceğiniz birçok assert yöntemi vardır. Her bir assert yöntemini açıklamak ve göstermek, kendi makalesini kolayca yapabilir, bu yüzden burada ayrıntılara girmeyeceğim.
Kullanılabilir bazı onaylama yöntemlerine örnekler: XCTAssertEqualObjects
, XCTAssertGreaterThan
, XCTAssertNil
, XCTAssertTrue
veya XCTAssertThrows
.
Mevcut assert yöntemleri hakkında daha fazla bilgiyi burada bulabilirsiniz.
Testi şimdi çalıştırırsanız, test yöntemi başarılı olacaktır. İlk test yönteminizi başarıyla oluşturdunuz, ancak henüz prime time için tam olarak hazır değil. Bu test yönteminin aşağıda ayrıntılı olarak açıklandığı gibi hala üç sorunu vardır (bir büyük ve iki küçük).
Sorun 1: NetworkService protokolünün gerçek uygulamasını kullanıyorsunuz
Birim testinin temel ilkelerinden biri, her testin herhangi bir dış faktör veya bağımlılıktan bağımsız olması gerektiğidir. Birim testleri atomik olmalıdır.
Bir noktada sunucudan bir API yöntemi çağıran bir yöntemi test ediyorsanız, testinizin ağ kodunuza ve sunucunun kullanılabilirliğine bağımlılığı vardır. Sunucu test sırasında çalışmıyorsa, testiniz başarısız olur ve bu nedenle yanlış bir şekilde test ettiğiniz yöntemi çalışmamakla suçlar.
Bu durumda, RegistryViewModel yöntemini test RegistrationViewModel
.
RegistrationViewModel
, test edilen yönteminiz emailValid
doğrudan NetworkServiceImpl
bağlı olmadığını bilseniz bile NetworkServiceImpl
sınıfına bağlıdır.
Birim testleri yazarken, tüm dış bağımlılıklar kaldırılmalıdır. Ancak, RegistryViewModel sınıfının uygulamasını değiştirmeden RegistrationViewModel
bağımlılığını nasıl kaldırmalısınız?
Bu sorunun kolay bir çözümü var ve buna Object Mocking deniyor. NetworkService
RegistrationViewModel
bağlı olduğunu görürsünüz.
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
RegistrationViewModel
başlatılırken, RegistrationViewModel
nesnesine NetworkService
protokolünün bir uygulaması verilir (veya enjekte edilir).
Bu ilke, yapıcı aracılığıyla bağımlılık enjeksiyonu olarak adlandırılır ( daha fazla bağımlılık enjeksiyonu türü vardır ).
Objc.io'daki bu makale gibi çevrimiçi bağımlılık enjeksiyonu hakkında birçok ilginç makale var.
Ayrıca burada bağımlılık enjeksiyonunu basit ve anlaşılır bir şekilde açıklayan kısa ama ilginç bir makale var.
Ek olarak, tek sorumluluk ilkesi ve DI hakkında harika bir makale Toptal blogunda mevcuttur.
RegistrationViewModel
başlatıldığında, yapıcısına bir NetworkService protokol uygulaması enjekte ediyor (bu nedenle bağımlılık enjeksiyon ilkesinin adı):
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
Görünüm modeli sınıfımız yalnızca protokole bağlı olduğundan, özel (veya alaylı) NetworkService
uygulama sınıfımızı oluşturmamızı ve alaylı sınıfı görünüm modeli nesnemize enjekte etmemizi engelleyen hiçbir şey yoktur.
Sahte NetworkService
protokol uygulamamızı oluşturalım.
Proje Gezgini'ndeki TestingIOSTests
klasörüne sağ tıklayarak test hedefimize yeni bir Swift dosyası ekleyin, “Yeni Dosya”yı seçin, “Swift dosyası”nı seçin ve adını NetworkServiceMock
olarak adlandırın.
Alaylı sınıfımız şöyle görünmelidir:
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) }) } }
Bu noktada, gerçek uygulamamızdan ( NetworkServiceImpl
) çok farklı değildir, ancak gerçek dünya durumunda, gerçek NetworkServiceImpl
bir ağ koduna, yanıt işlemeye ve benzer işlevselliğe sahip olacaktır.
Alay sınıfımız hiçbir şey yapmaz, alaylı sınıfın amacı budur. Hiçbir şey yapmazsa, testlerimize müdahale etmeyecektir.
Testimizin ilk sorununu düzeltmek için, aşağıdakileri değiştirerek test yöntemimizi güncelleyelim:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
ile birlikte:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
Sorun 2: Test yöntemi gövdesinde kayıt VM'sini başlatıyorsunuz
Bir nedenden dolayı tearDown
ve setUp
yöntemleri var.
Bu yöntemler, bir testte gerekli olan tüm gerekli nesneleri başlatmak veya kurmak için kullanılır. Her test yönteminde aynı init veya setup yöntemlerini yazarak kod tekrarını önlemek için bu yöntemleri kullanmalısınız. Özellikle belirli bir test yöntemi için gerçekten özel bir yapılandırmanız varsa, kurulum ve yırtma yöntemlerini kullanmamak her zaman büyük bir sorun değildir.
RegistrationViewModel
sınıfını başlatmamız oldukça basit olduğundan, kurulum ve yırtma yöntemlerini kullanmak için test sınıfınızı yeniden değerlendireceksiniz.
RegistrationViewModelTests
şöyle görünmelidir:
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") ... } }
Sorun 3: Bir test yönteminde birden çok onayınız var
Bu büyük bir sorun olmasa da, yöntem başına bir iddiaya sahip olmanın bazı savunucuları var.
Bu ilkenin ana mantığı hata tespitidir.
Bir test yönteminin birden fazla onaylaması varsa ve ilki başarısız olursa, tüm test yöntemi başarısız olarak işaretlenir. Diğer iddialar test bile edilmeyecek.
Bu şekilde bir seferde yalnızca bir hata keşfedersiniz. Diğer iddiaların başarısız mı yoksa başarılı mı olacağını bilemezsiniz.
Tek bir yöntemde birden fazla iddiaya sahip olmak her zaman kötü bir şey değildir çünkü bir seferde yalnızca bir hatayı düzeltebilirsiniz, bu nedenle bir seferde bir hatayı tespit etmek o kadar büyük bir sorun olmayabilir.

Bizim durumumuzda, bir e-posta formatının geçerliliği test edilir. Bu yalnızca bir işlev olduğundan, testin daha kolay okunmasını ve anlaşılmasını sağlamak için tüm iddiaları tek bir yöntemde gruplandırmak daha mantıklı olabilir.
Bu sorun aslında büyük bir sorun olmadığından ve hatta bazıları bunun bir sorun olmadığını iddia edebileceğinden, test yönteminizi olduğu gibi tutacaksınız.
Kendi birim testlerinizi yazdığınızda, her bir test yöntemi için hangi yolu izlemek istediğinize karar vermek size kalmıştır. Büyük olasılıkla, test başına felsefenin öne sürülmesinin mantıklı olduğu ve olmadığı yerlerde başka yerler olduğunu göreceksiniz.
Asenkron Çağrılarla Test Yöntemleri
Uygulama ne kadar basit olursa olsun, özellikle UI'nin kendi iş parçacığında yürütülmesini istediğiniz için, başka bir iş parçacığında eşzamansız olarak yürütülmesi gereken bir yöntem olma olasılığı yüksektir.
Birim testi ve eşzamansız çağrılarla ilgili ana sorun, eşzamansız bir çağrının tamamlanmasının zaman alması, ancak birim testinin bitene kadar beklememesidir. Birim testi, bir zaman uyumsuz bloğun içindeki herhangi bir kod yürütülmeden önce bittiği için, testimiz her zaman aynı sonuçla sona erecektir (zaman uyumsuz bloğunuza ne yazarsanız yazın).
Bunu göstermek için checkEmailAvailability
yöntemi için bir test oluşturalım.
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") } }
Burada, yöntemimiz size e-postanın mevcut olmadığını (zaten başka bir kullanıcı tarafından alınmış) söyledikten sonra bir recordEnabled değişkeninin false olarak ayarlanıp ayarlanmadığını test etmek istiyorsunuz.
Bu testi yaparsanız, geçer. Ama bir şey daha dene. İddianızı şu şekilde değiştirin:
XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")
Testi tekrar çalıştırırsanız, tekrar geçer.
Bunun nedeni, iddiamızın ileri sürülmemiş olmasıdır. Birim testi, geri arama bloğu yürütülmeden önce sona erdi (alaylı ağ hizmeti uygulamamızda, geri dönmeden önce bir saniye beklemeye ayarlandığını unutmayın).
Neyse ki, Xcode 6 ile Apple, XCTest çerçevesine XCTestExpectation
sınıfı olarak test beklentilerini ekledi. XCTestExpectation
sınıfı şu şekilde çalışır:
- Testin başında, testten ne beklediğinizi açıklayan basit bir metinle test beklentinizi belirlersiniz.
- Zaman uyumsuz bir blokta, test kodunuz yürütüldükten sonra beklentiyi yerine getirirsiniz.
- Testin sonunda
waitForExpectationWithTimer
bloğunu ayarlamanız gerekir. Beklenti gerçekleştiğinde veya zamanlayıcı bittiğinde - hangisi önce gerçekleşirse - yürütülür. - Şimdi, birim testi, beklenti karşılanana veya beklenti zamanlayıcısı bitene kadar bitmeyecek.
XCTestExpectation
sınıfını kullanmak için testimizi yeniden yazalım.
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") } } }
Testi şimdi çalıştırırsanız, olması gerektiği gibi başarısız olur. Geçmesi için testi düzeltelim. İddiayı şu şekilde değiştirin:
XCTAssert(self.registrationVM.registrationEnabled.value == false, "Email address is not available, registration should be disabled")
Başarılı olduğunu görmek için testi tekrar çalıştırın. Beklenti zamanlayıcısı biterse ne olacağını görmek için ağ hizmeti alaylı uygulamasında gecikme süresini değiştirmeyi deneyebilirsiniz.
Geri Çağırmadan Eşzamansız Çağrılarla Test Yöntemleri
Örnek proje yöntemimiz attemptUserRegistration
, zaman uyumsuz olarak yürütülen kodu içeren NetworkService.attemptRegistration
yöntemini kullanır. Yöntem, bir kullanıcıyı arka uç hizmetine kaydetmeye çalışır.
Demo uygulamamızda, yöntem bir ağ aramasını simüle etmek ve başarılı bir kaydı taklit etmek için sadece bir saniye bekleyecektir. Kayıt başarılı olursa loginSuccessful
değeri true olarak ayarlanır. Bu davranışı doğrulamak için bir birim testi yapalım.
func testAttemptRegistration() { registrationVM.emailAddress = "[email protected]" registrationVM.password = "123456" registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, "Login must be successful") }
Çalıştırılırsa, loginSuccessful
değeri eşzamansız networkService.attemptRegistration
yöntemi bitene kadar true olarak ayarlanmayacağından bu test başarısız olur.
Başarılı bir kayıt döndürmeden önce attemptRegistration
yönteminin bir saniye bekleyeceği alaylı bir NetworkServiceImpl
oluşturduğunuz için, Grand Central Dispatch'i (GCD) kullanabilir ve bir saniye sonra onayınızı kontrol etmek için asyncAfter
yöntemini kullanabilirsiniz. asyncAfter
kodunu ekledikten sonra test kodumuz şöyle görünecektir:
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") } }
Dikkat ettiyseniz, bunun hala işe yaramayacağını bileceksiniz çünkü test yöntemi asyncAfter
bloğu yürütülmeden önce yürütülecek ve sonuç olarak yöntem her zaman başarılı bir şekilde geçecektir. Neyse ki, XCTestException
sınıfı var.
XCTestException
sınıfını kullanmak için yöntemimizi yeniden yazalım:
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") } } }
RegistrationViewModel
kapsayan birim testleri ile artık yeni işlevlerin eklenmesinin veya mevcut işlevlerin güncellenmesinin hiçbir şeyi bozmayacağından emin olabilirsiniz.
Önemli Not: Birim testleri, kapsadıkları yöntemlerin işlevselliği değiştiğinde güncellenmezlerse değerlerini kaybederler. Birim testleri yazmak, uygulamanın geri kalanına ayak uydurması gereken bir süreçtir.
İpucu: Yazma testlerini sonuna kadar ertelemeyin. Geliştirme sırasında testler yazın. Bu şekilde, neyin test edilmesi gerektiğini ve sınır vakalarının neler olduğunu daha iyi anlayacaksınız.
UI Testleri Yazma
Tüm birim testleri tamamen geliştirilip başarıyla yürütüldükten sonra, her bir kod biriminin doğru çalıştığından emin olabilirsiniz, ancak bu, uygulamanızın bir bütün olarak amaçlandığı gibi çalıştığı anlamına mı geliyor?
İşte bu noktada UI testleri önemli bir bileşen olan entegrasyon testleri devreye girer.
UI testine başlamadan önce, test edilecek bazı UI öğeleri ve etkileşimleri (veya kullanıcı hikayeleri) olması gerekir. Basit bir görünüm ve onun görünüm denetleyicisini oluşturalım.
-
Main.storyboard
açın ve aşağıdaki resimdeki gibi görünecek basit bir görünüm denetleyicisi oluşturun.
E-posta metin alanı etiketini 100'e, şifre metin alanı etiketini 101'e ve şifre onay etiketini 102'ye ayarlayın.
- Yeni bir görünüm denetleyici dosyası
RegistrationViewController.swift
ekleyin ve tüm çıkışları storyboard'a bağlayın.
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() } }
Burada sınıfa IBOutlets
ve TextFieldTags
yapısı ekliyorsunuz.
Bu, hangi metin alanının düzenlenmekte olduğunu belirlemenizi mümkün kılacaktır. Görünüm modelinde Dinamik özellikleri kullanmak için, görünüm denetleyicisindeki dinamik özellikleri 'bağlamanız' gerekir. Bunu bindViewModel
yönteminde yapabilirsiniz:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = $0 } } }
Şimdi, metin alanlarından herhangi birinin ne zaman güncellendiğini takip etmek için bir metin alanı temsilci yöntemi ekleyelim:
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 }
- Görünüm denetleyicisini uygun görünüm modeline bağlamak için
AppDelegate
güncelleyin (bu adımın MVVM mimarisinin bir gereksinimi olduğunu unutmayın). GüncellenmişAppDelegate
kodu daha sonra şöyle görünmelidir:
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 } }
Film şeridi dosyası ve RegistrationViewController
gerçekten basittir, ancak otomatikleştirilmiş UI testinin nasıl çalıştığını göstermek için yeterlidir.
Her şey düzgün ayarlanmışsa, uygulama başladığında kayıt düğmesi devre dışı bırakılmalıdır. Yalnızca tüm alanlar doldurulduğunda ve geçerli olduğunda, kayıt düğmesi etkinleştirilmelidir.
Bu ayarlandıktan sonra ilk UI testinizi oluşturabilirsiniz.
UI testimiz, Kayıt düğmesinin etkinleştirilip etkinleştirilmeyeceğini, yalnızca geçerli bir e-posta adresi, geçerli bir parola ve geçerli bir parola onayının tümü girilmişse kontrol etmelidir. Bunu nasıl kuracağınız aşağıda açıklanmıştır:
-
TestingIOSUITests.swift
dosyasını açın. -
testExample()
testRegistrationButtonEnabled()
ekleyin. - İmleci, oraya bir şeyler yazacakmış gibi
testRegistrationButtonEnabled
yöntemine getirin. - Kullanıcı Arayüzü testini kaydet düğmesine basın (ekranın altındaki kırmızı daire).
- Kayıt düğmesine basıldığında uygulama başlatılacaktır.
- Uygulama başlatıldıktan sonra e-posta metin alanına dokunun ve '[email protected]' yazın. Kodun otomatik olarak test yöntemi gövdesinde göründüğünü fark edeceksiniz.
Bu özelliği kullanarak tüm UI talimatlarını kaydedebilirsiniz, ancak basit talimatları manuel olarak yazmanın çok daha hızlı olacağını görebilirsiniz.
Bu, bir şifre metin alanına dokunmak ve bir e-posta adresi '[email protected]' girmek için bir kaydedici talimatının bir örneğidir.
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]")
- Test etmek istediğiniz UI etkileşimleri kaydedildikten sonra, kaydı durdurmak için durdur düğmesine (kayıt düğmesi etiketi, kaydetmeye başladığınızda duracak şekilde değiştirildi) tekrar basın.
- UI etkileşimleri kaydedicinizi aldıktan sonra, artık uygulamanın çeşitli durumlarını veya UI öğelerini test etmek için çeşitli
XCTAsserts
ekleyebilirsiniz.
Kaydedilen talimatlar her zaman kendi kendini açıklayıcı değildir ve hatta tüm test yöntemini okumayı ve anlamayı biraz zorlaştırabilir. Neyse ki, kullanıcı arayüzü talimatlarını manuel olarak girebilirsiniz.
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.