วิธีเขียนการทดสอบอัตโนมัติสำหรับ iOS

เผยแพร่แล้ว: 2022-03-11

ในฐานะนักพัฒนาที่ดี คุณพยายามอย่างเต็มที่เพื่อทดสอบฟังก์ชันการทำงานทั้งหมด รวมถึงเส้นทางโค้ดที่เป็นไปได้ทั้งหมดและผลลัพธ์ในซอฟต์แวร์ที่คุณเขียน แต่เป็นเรื่องยากมากและไม่ปกติที่จะสามารถทดสอบผลลัพธ์ที่เป็นไปได้ทั้งหมดและทุกเส้นทางที่ผู้ใช้อาจใช้ด้วยตนเอง

เมื่อแอปพลิเคชันมีขนาดใหญ่และซับซ้อนมากขึ้น โอกาสที่คุณจะพลาดบางอย่างจากการทดสอบด้วยตนเองก็เพิ่มขึ้นอย่างมาก

การทดสอบอัตโนมัติ ทั้ง UI และ API ของบริการส่วนหลัง จะทำให้คุณมั่นใจมากขึ้นว่าทุกอย่างทำงานตามที่ตั้งใจไว้ และจะช่วยลดความเครียดเมื่อพัฒนา ปรับโครงสร้างใหม่ เพิ่มคุณสมบัติใหม่ หรือเปลี่ยนแปลงคุณสมบัติที่มีอยู่

ด้วยการทดสอบอัตโนมัติ คุณสามารถ:

  • ลดจุดบกพร่อง: ไม่มีวิธีใดที่จะลบความเป็นไปได้ของจุดบกพร่องในโค้ดของคุณได้อย่างสมบูรณ์ แต่การทดสอบอัตโนมัติสามารถลดจำนวนจุดบกพร่องได้อย่างมาก
  • ทำการเปลี่ยนแปลงอย่างมั่นใจ: หลีกเลี่ยงจุดบกพร่องเมื่อเพิ่มคุณสมบัติใหม่ ซึ่งหมายความว่า คุณสามารถทำการเปลี่ยนแปลงได้อย่างรวดเร็วและไม่ลำบาก
  • บันทึกรหัสของเรา: เมื่อดูการทดสอบ เราจะเห็นได้ชัดเจนว่าฟังก์ชันบางอย่างคาดหวัง เงื่อนไขคืออะไร และกรณีมุมคืออะไร
  • ปรับโครงสร้างใหม่อย่างไม่ลำบากใจ: ในฐานะนักพัฒนา บางครั้งคุณอาจกลัวการจัดโครงสร้างใหม่ โดยเฉพาะอย่างยิ่งหากคุณต้องการจัดโครงสร้างโค้ดใหม่จำนวนมาก การทดสอบหน่วยอยู่ที่นี่เพื่อให้แน่ใจว่าโค้ดที่ปรับโครงสร้างใหม่ยังคงทำงานตามที่ตั้งใจไว้

บทความนี้จะสอนวิธีจัดโครงสร้างและดำเนินการทดสอบอัตโนมัติบนแพลตฟอร์ม iOS

การทดสอบหน่วยเทียบกับการทดสอบ UI

สิ่งสำคัญคือต้องแยกความแตกต่างระหว่างการทดสอบหน่วยและ UI

การ ทดสอบหน่วยจะ ทดสอบ ฟังก์ชันเฉพาะ ภายใต้ บริบทเฉพาะ การทดสอบหน่วยตรวจสอบว่าส่วนที่ทดสอบแล้วของโค้ด (โดยปกติคือฟังก์ชันเดียว) ทำในสิ่งที่ควรทำ มีหนังสือและบทความมากมายเกี่ยวกับการทดสอบหน่วยการเรียนรู้ ดังนั้นเราจะไม่กล่าวถึงในโพสต์นี้

การทดสอบ UI ใช้สำหรับทดสอบส่วนต่อประสานผู้ใช้ ตัวอย่างเช่น ช่วยให้คุณทดสอบว่าข้อมูลพร็อพเพอร์ตี้ได้รับการอัปเดตตามที่ตั้งใจไว้หรือมีการทริกเกอร์การทำงานเฉพาะตามที่ควรจะเป็นเมื่อผู้ใช้โต้ตอบกับองค์ประกอบ UI บางอย่าง

การทดสอบ UI แต่ละครั้งจะทดสอบการ โต้ตอบของผู้ใช้ที่เฉพาะเจาะจง กับ UI ของแอปพลิเคชัน การทดสอบอัตโนมัติสามารถทำได้และควรทำทั้งในระดับการทดสอบหน่วยและการทดสอบ UI

การตั้งค่าการทดสอบอัตโนมัติ

เนื่องจาก XCode รองรับการทดสอบหน่วยและ UI ทันที การเพิ่มลงในโครงการของคุณจึงเป็นเรื่องง่ายและตรงไปตรงมา เมื่อสร้างโครงการใหม่ ให้เลือก "รวมการทดสอบหน่วย" และ "รวมการทดสอบ UI"

เมื่อสร้างโปรเจ็กต์แล้ว เป้าหมายใหม่สองรายการจะถูกเพิ่มลงในโปรเจ็กต์ของคุณเมื่อตรวจสอบสองตัวเลือกนี้แล้ว ชื่อเป้าหมายใหม่มี "การทดสอบ" หรือ "UITests" ต่อท้ายชื่อ

แค่นั้นแหละ. คุณพร้อมที่จะเขียนการทดสอบอัตโนมัติสำหรับโครงการของคุณ

ภาพ: การตั้งค่าการทดสอบอัตโนมัติใน XCode

หากคุณมีโปรเจ็กต์อยู่แล้วและต้องการเพิ่มการรองรับ UI และการทดสอบหน่วย คุณจะต้องทำงานเพิ่มขึ้นอีกเล็กน้อย แต่ก็ตรงไปตรงมาและเรียบง่ายมาก

ไปที่ ไฟล์→ใหม่→เป้าหมาย และเลือก iOS Unit Testing Bundle สำหรับการทดสอบหน่วยหรือ iOS UI Testing Bundle สำหรับการทดสอบ UI

รูปภาพ: การเลือกบันเดิลการทดสอบหน่วย iOS

กด ถัดไป

ในหน้าจอตัวเลือกเป้าหมาย คุณสามารถปล่อยให้ทุกอย่างเป็นไปตามที่มันเป็น (หากคุณมีหลายเป้าหมายและต้องการทดสอบเฉพาะเป้าหมาย ให้เลือกเป้าหมายในดรอปดาวน์เป้าหมายที่จะทดสอบ)

กด เสร็จสิ้น ทำขั้นตอนนี้ซ้ำสำหรับการทดสอบ UI แล้วคุณจะมีทุกอย่างพร้อมสำหรับเริ่มเขียนการทดสอบอัตโนมัติในโปรเจ็กต์ที่มีอยู่ของคุณ

การเขียนแบบทดสอบหน่วย

ก่อนที่เราจะเริ่มต้นเขียนการทดสอบหน่วย เราต้องเข้าใจกายวิภาคของพวกมันเสียก่อน เมื่อคุณรวมการทดสอบหน่วยในโครงการของคุณ คลาสการทดสอบตัวอย่างจะถูกสร้างขึ้น ในกรณีของเราจะมีลักษณะดังนี้:

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

วิธีที่สำคัญที่สุดในการทำความเข้าใจคือ setUp และ tearDown เมธอด setUp จะถูกเรียก ก่อน ทุกวิธีทดสอบ ในขณะที่เมธอด tearDown จะถูกเรียก หลัง ทุกวิธีทดสอบ หากเรารันการทดสอบที่กำหนดไว้ในคลาสการทดสอบตัวอย่างนี้ เมธอดจะทำงานดังนี้:

setUp → testExample → tearDown setUp → testPerformanceExample → tearDown

เคล็ดลับ: การทดสอบทำได้โดยการกด cmd + U โดยเลือก Product → Test หรือโดยการคลิกปุ่ม Run ค้างไว้จนกระทั่งเมนูตัวเลือกปรากฏขึ้น จากนั้นเลือก Test จากเมนู

หากคุณต้องการเรียกใช้วิธีทดสอบเฉพาะวิธีเดียวเท่านั้น ให้กดปุ่มทางด้านซ้ายของชื่อวิธี (แสดงในภาพด้านล่าง)

ภาพ: การเลือกวิธีการทดสอบเฉพาะ

ตอนนี้ เมื่อคุณมีทุกอย่างพร้อมสำหรับการเขียนแบบทดสอบแล้ว คุณสามารถเพิ่มคลาสตัวอย่างและวิธีการทดสอบบางอย่างได้

เพิ่มคลาสที่จะรับผิดชอบการลงทะเบียนผู้ใช้ ผู้ใช้ป้อนที่อยู่อีเมล รหัสผ่าน และการยืนยันรหัสผ่าน คลาสตัวอย่างของเราจะตรวจสอบการป้อนข้อมูล ตรวจสอบความพร้อมใช้งานของที่อยู่อีเมล และพยายามลงทะเบียนผู้ใช้

หมายเหตุ: ตัวอย่างนี้ใช้รูปแบบสถาปัตยกรรม MVVM (หรือ Model-View-ViewModel)

ใช้ MVVM เนื่องจากทำให้สถาปัตยกรรมของแอปพลิเคชันสะอาดขึ้นและทดสอบได้ง่ายขึ้น

ด้วย MVVM การแยกตรรกะทางธุรกิจออกจากตรรกะการนำเสนอจะง่ายกว่า จึงหลีกเลี่ยงปัญหาตัวควบคุมการดูจำนวนมาก

รายละเอียดเกี่ยวกับสถาปัตยกรรม MVVM อยู่นอกขอบเขตของบทความนี้ แต่คุณสามารถอ่านเพิ่มเติมเกี่ยวกับสถาปัตยกรรมนี้ได้ในบทความนี้

มาสร้างคลาส view-model ที่รับผิดชอบการลงทะเบียนผู้ใช้กัน .

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

ขั้นแรก เราได้เพิ่มคุณสมบัติสองสามอย่าง คุณสมบัติไดนามิก และเมธอด init

ไม่ต้องกังวลกับประเภท Dynamic เป็นส่วนหนึ่งของสถาปัตยกรรม MVVM

เมื่อตั้งค่า Dynamic<Bool> เป็นจริง ตัวควบคุมการดูที่ถูกผูกไว้ (เชื่อมต่อ) กับ RegistrationViewModel จะเปิดใช้งานปุ่มการลงทะเบียน เมื่อตั้งค่า loginSuccessful เป็น true มุมมองที่เชื่อมต่อจะอัปเดตตัวเอง

มาเพิ่มวิธีการตรวจสอบความถูกต้องของรหัสผ่านและรูปแบบอีเมลกัน

 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 }

ทุกครั้งที่ผู้ใช้พิมพ์บางอย่างในอีเมลหรือฟิลด์รหัสผ่าน เมธอด enableRegistrationAttempt จะตรวจสอบว่าอีเมลและรหัสผ่านอยู่ในรูปแบบที่ถูกต้องหรือไม่ และเปิดหรือปิดใช้งานปุ่มการลง registrationEnabled ผ่านคุณสมบัติไดนามิก registerEnabled

เพื่อให้ตัวอย่างง่าย ให้เพิ่มสองวิธีง่ายๆ หนึ่งวิธีเพื่อตรวจสอบความพร้อมของอีเมล และอีกวิธีหนึ่งเพื่อลองลงทะเบียนด้วยชื่อผู้ใช้และรหัสผ่านที่ให้มา

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

สองวิธีนี้ใช้ NetworkService เพื่อตรวจสอบว่ามีอีเมลหรือไม่และพยายามลงทะเบียน

เพื่อให้ตัวอย่างนี้ง่าย การใช้งาน NetworkService ไม่ได้ใช้ API แบ็คเอนด์ใดๆ แต่เป็นเพียงต้นขั้วที่ปลอมแปลงผลลัพธ์ NetworkService ถูกนำมาใช้เป็นโปรโตคอลและคลาสการใช้งาน

 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 เป็นโปรโตคอลที่ง่ายมาก มีเพียงสองวิธี: ความพยายามในการลงทะเบียน และวิธีการตรวจสอบความพร้อมใช้งานของอีเมล การใช้โปรโตคอลเป็นคลาส 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) }) } }

ทั้งสองวิธีเพียงแค่รอสักครู่ (แกล้งหน่วงเวลาของคำขอเครือข่าย) แล้วเรียกวิธีการโทรกลับที่เหมาะสม

เคล็ดลับ: เป็นการดีที่จะใช้โปรโตคอล (หรือที่เรียกว่าอินเทอร์เฟซในภาษาการเขียนโปรแกรมอื่นๆ) คุณสามารถอ่านเพิ่มเติมเกี่ยวกับเรื่องนี้ได้หากคุณค้นหา 'หลักการเขียนโปรแกรมไปยังอินเทอร์เฟซ' คุณยังจะได้เห็นว่ามันเล่นได้ดีเพียงใดกับการทดสอบหน่วย

ตอนนี้ เมื่อตัวอย่างถูกตั้งค่า เราสามารถเขียนการทดสอบหน่วยเพื่อให้ครอบคลุมวิธีการของคลาสนี้

  1. สร้างคลาสทดสอบใหม่สำหรับโมเดลการดูของเรา คลิกขวาที่โฟลเดอร์ TestingIOSTests ในบานหน้าต่าง Project Navigator เลือก New File → Unit Test Case Class และตั้งชื่อว่า RegistrationViewModelTests

  2. ลบ testExample และ testPerformanceExample เนื่องจากเราต้องการสร้างวิธีการทดสอบของเราเอง

  3. เนื่องจาก Swift ใช้โมดูลและการทดสอบของเราอยู่ในโมดูลที่แตกต่างจากโค้ดของแอปพลิเคชัน เราจึงต้องนำเข้าโมดูลของแอปพลิเคชันเป็น @testable ด้านล่างคำสั่งนำเข้าและคำจำกัดความของคลาส ให้เพิ่ม @testable import TestingIOS (หรือชื่อโมดูลของแอปพลิเคชันของคุณ) หากไม่มีสิ่งนี้ เราจะไม่สามารถอ้างอิงคลาสหรือเมธอดของแอปพลิเคชันของเราได้

  4. เพิ่มตัวแปร registrationViewModel

นี่คือลักษณะของคลาสทดสอบที่ว่างเปล่าของเราในตอนนี้:

 import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }

มาลองเขียนแบบทดสอบสำหรับเมธอด emailValid กัน เราจะสร้างวิธีการทดสอบใหม่ที่เรียกว่า testEmailValid สิ่งสำคัญคือต้องเพิ่มคำสำคัญ test ที่จุดเริ่มต้นของชื่อ มิฉะนั้น จะไม่รู้จักวิธีการนี้ว่าเป็นวิธีทดสอบ

วิธีทดสอบของเรามีลักษณะดังนี้:

 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") }

วิธีการทดสอบของเราใช้วิธีการยืนยัน XCTAssert ซึ่งในกรณีของเราจะตรวจสอบว่าเงื่อนไขเป็นจริงหรือเท็จ

หากเงื่อนไขเป็นเท็จ การยืนยันจะล้มเหลว (พร้อมกับการทดสอบ) และข้อความของเราจะถูกเขียนออกมา

มีวิธีการยืนยันมากมายที่คุณสามารถใช้ในการทดสอบของคุณได้ การอธิบายและแสดงวิธีการยืนยันแต่ละวิธีสามารถสร้างบทความของตัวเองได้ง่ายๆ ฉันจะไม่ลงรายละเอียดที่นี่

ตัวอย่างบางส่วนของวิธีการยืนยันที่ใช้ได้ ได้แก่ XCTAssertEqualObjects , XCTAssertGreaterThan , XCTAssertNil , XCTAssertTrue หรือ XCTAssertThrows

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับวิธีการยืนยันได้ที่นี่

หากคุณเรียกใช้การทดสอบตอนนี้ วิธีการทดสอบจะผ่าน คุณสร้างวิธีทดสอบแรกสำเร็จแล้ว แต่ยังไม่พร้อมสำหรับช่วงไพร์มไทม์ วิธีทดสอบนี้ยังคงมีสามประเด็น (ปัญหาใหญ่หนึ่งเรื่องและเรื่องเล็กสองประเด็น) ตามรายละเอียดด้านล่าง

ปัญหาที่ 1: คุณกำลังใช้การใช้งานจริงของโปรโตคอล NetworkService

หลักการสำคัญประการหนึ่งของการทดสอบหน่วยคือการทดสอบทุกครั้งควรไม่ขึ้นกับปัจจัยภายนอกหรือการพึ่งพาอาศัยกัน การทดสอบหน่วยควรเป็นแบบปรมาณู

หากคุณกำลังทดสอบเมธอด ซึ่งในบางจุดเรียกเมธอด API จากเซิร์ฟเวอร์ การทดสอบของคุณจะขึ้นอยู่กับโค้ดเครือข่ายและความพร้อมใช้งานของเซิร์ฟเวอร์ หากเซิร์ฟเวอร์ไม่ทำงานในขณะที่ทำการทดสอบ การทดสอบของคุณจะล้มเหลว ดังนั้นจึงเป็นการกล่าวหาว่าวิธีที่ทดสอบของคุณไม่ทำงานอย่างไม่ถูกต้อง

ในกรณีนี้ คุณกำลังทดสอบวิธีการของ RegistrationViewModel

RegistrationViewModel ขึ้นอยู่กับคลาส NetworkServiceImpl แม้ว่าคุณจะรู้ว่าวิธีทดสอบของคุณ emailValid ไม่ได้ขึ้นอยู่กับ NetworkServiceImpl โดยตรง

เมื่อเขียนการทดสอบหน่วย ควรลบการพึ่งพาภายนอกทั้งหมดออก แต่คุณควรลบการพึ่งพา NetworkService โดยไม่เปลี่ยนการใช้งานคลาส RegistrationViewModel อย่างไร

มีวิธีแก้ปัญหาที่ง่ายสำหรับปัญหานี้ เรียกว่า Object Mocking หากคุณดู RegistrationViewModel อย่างใกล้ชิด คุณจะเห็นว่าจริงๆ แล้วขึ้นอยู่กับโปรโตคอล NetworkService

 class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...

เมื่อมีการเริ่มต้น RegistrationViewModel การใช้งานโปรโตคอล NetworkService จะได้รับ (หรือฉีด) ให้กับอ็อบเจ็กต์ RegistrationViewModel

หลักการนี้เรียกว่า การฉีดการพึ่งพาผ่านตัวสร้าง ( มีการฉีดการพึ่งพาประเภทต่างๆ มากขึ้น )

มีบทความที่น่าสนใจมากมายเกี่ยวกับการแทรกการพึ่งพาออนไลน์ เช่น บทความนี้ใน objc.io

นอกจากนี้ยังมีบทความสั้น ๆ แต่น่าสนใจที่อธิบายการฉีดพึ่งพาในวิธีที่ง่ายและตรงไปตรงมาที่นี่

นอกจากนี้ยังมีบทความดีๆ เกี่ยวกับหลักการความรับผิดชอบเดียวและ DI ที่บล็อก Toptal

เมื่อ RegistrationViewModel ถูกสร้างขึ้น มันจะฉีดการนำโปรโตคอล NetworkService ไปใช้ในตัวสร้าง (ด้วยเหตุนี้ จึงเป็นชื่อของหลักการการฉีดขึ้นต่อกัน):

 let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())

เนื่องจากคลาสโมเดลการดูของเราขึ้นอยู่กับโปรโตคอลเท่านั้น จึงไม่มีอะไรหยุดเราไม่ให้สร้างคลาสการใช้งาน NetworkService แบบกำหนดเอง (หรือจำลอง) และฉีดคลาสจำลองลงในอ็อบเจ็กต์โมเดลการดูของเรา

มาสร้างการใช้งานโปรโตคอล NetworkService ของเรากันเถอะ

เพิ่มไฟล์ Swift ใหม่ให้กับเป้าหมายการทดสอบของเราโดยคลิกขวาที่โฟลเดอร์ TestingIOSTests ใน Project Navigator เลือก "ไฟล์ใหม่" เลือก "ไฟล์ Swift" และตั้งชื่อเป็น NetworkServiceMock

นี่คือลักษณะของชั้นเรียนเยาะเย้ยของเรา:

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

ณ จุดนี้ ไม่ได้แตกต่างจากการใช้งานจริงของเรา ( NetworkServiceImpl ) มากนัก แต่ในสถานการณ์จริง NetworkServiceImpl จริงจะมีรหัสเครือข่าย การจัดการการตอบสนอง และฟังก์ชันการทำงานที่คล้ายคลึงกัน

ชั้นเรียนเยาะเย้ยของเราไม่ได้ทำอะไรเลยซึ่งเป็นประเด็นของชั้นเรียนที่เยาะเย้ย หากไม่ได้ทำอะไรมากกว่านั้นจะไม่รบกวนการทดสอบของเรา

ในการแก้ไขปัญหาแรกของการทดสอบ เรามาอัปเดตวิธีการทดสอบโดยแทนที่:

 let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())

กับ:

 let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())

ปัญหา 2: คุณกำลังสร้างอินสแตนซ์ของ registerVM ในเนื้อหาวิธีการทดสอบ

มีวิธี tearDown setUp เหตุผล

วิธีการเหล่านั้นใช้เพื่อเริ่มต้นหรือตั้งค่าวัตถุที่จำเป็นทั้งหมดที่จำเป็นในการทดสอบ คุณควรใช้วิธีการเหล่านั้นเพื่อหลีกเลี่ยงการทำซ้ำโค้ดโดยเขียนวิธี init หรือการตั้งค่าเดียวกันในทุกวิธีการทดสอบ การไม่ใช้เมธอด setup และ tearDown ไม่ได้เป็นปัญหาใหญ่เสมอไป โดยเฉพาะอย่างยิ่งหากคุณมีการกำหนดค่าเฉพาะจริงๆ สำหรับวิธีทดสอบเฉพาะ

เนื่องจากการเริ่มต้นคลาส RegistrationViewModel ของเรานั้นค่อนข้างง่าย คุณจะต้องปรับโครงสร้างคลาสการทดสอบใหม่เพื่อใช้เมธอด setup และ tearDown

RegistrationViewModelTests ควรมีลักษณะดังนี้:

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

ปัญหา 3: คุณมีการยืนยันหลายครั้งในวิธีการทดสอบเดียว

แม้ว่านี่ไม่ใช่ปัญหาใหญ่ แต่ก็มีผู้ให้การสนับสนุนบางคนที่มีการยืนยันหนึ่งวิธีต่อหนึ่งวิธี

เหตุผลหลักสำหรับหลักการนี้คือการตรวจจับข้อผิดพลาด

หากวิธีการทดสอบหนึ่งมีการยืนยันหลายครั้งและวิธีแรกล้มเหลว วิธีการทดสอบทั้งหมดจะถูกทำเครื่องหมายว่าล้มเหลว คำยืนยันอื่น ๆ จะไม่ได้รับการทดสอบด้วยซ้ำ

วิธีนี้คุณจะค้นพบข้อผิดพลาดเพียงครั้งเดียว คุณจะไม่ทราบว่าคำยืนยันอื่น ๆ จะล้มเหลวหรือประสบความสำเร็จ

ไม่ใช่เรื่องเลวร้ายเสมอไปที่จะมีการยืนยันหลายรายการในวิธีเดียว เนื่องจากคุณสามารถแก้ไขข้อผิดพลาดได้ครั้งละหนึ่งรายการเท่านั้น ดังนั้นการตรวจหาข้อผิดพลาดครั้งละหนึ่งรายการอาจไม่เป็นปัญหาใหญ่นัก

ในกรณีของเรา จะมีการทดสอบความถูกต้องของรูปแบบอีเมล เนื่องจากนี่เป็นเพียงฟังก์ชันเดียว การจัดกลุ่มคำยืนยันทั้งหมดไว้ด้วยกันในวิธีเดียวจึงอาจสมเหตุสมผลกว่า เพื่อทำให้การทดสอบอ่านและทำความเข้าใจได้ง่ายขึ้น

เนื่องจากปัญหานี้ไม่ใช่ปัญหาใหญ่จริง ๆ และบางคนอาจโต้แย้งว่ามันไม่ใช่ปัญหาเลย คุณจะรักษาวิธีการทดสอบของคุณไว้เหมือนเดิม

เมื่อคุณเขียนการทดสอบหน่วยของคุณเอง คุณจะต้องตัดสินใจว่าจะเลือกเส้นทางใดสำหรับวิธีการทดสอบแต่ละวิธี เป็นไปได้มากว่าคุณจะพบว่ามีสถานที่หนึ่งที่ปรัชญาการทดสอบหนึ่งข้อยืนยันได้อย่างเหมาะสม และที่อื่นๆ ที่ไม่เป็นไปตามหลักปรัชญาการทดสอบ

วิธีการทดสอบด้วยการโทรแบบอะซิงโครนัส

ไม่ว่าแอปพลิเคชันจะเรียบง่ายเพียงใด ก็มีโอกาสสูงที่จะมีวิธีการที่จำเป็นต้องดำเนินการกับเธรดอื่นแบบอะซิงโครนัส โดยเฉพาะอย่างยิ่งเนื่องจากคุณมักจะต้องการให้ UI ดำเนินการในเธรดของตัวเอง

ปัญหาหลักเกี่ยวกับการทดสอบหน่วยและการเรียกแบบอะซิงโครนัสคือการเรียกแบบอะซิงโครนัสต้องใช้เวลาจึงจะเสร็จสิ้น แต่การทดสอบหน่วยจะไม่รอจนกว่าจะเสร็จสิ้น เนื่องจากการทดสอบหน่วยเสร็จสิ้นก่อนที่จะดำเนินการโค้ดใดๆ ภายในบล็อก async การทดสอบของเราจะจบลงด้วยผลลัพธ์เดียวกันเสมอ (ไม่ว่าคุณจะเขียนอะไรในบล็อก async ของคุณ)

เพื่อแสดงสิ่งนี้ มาสร้างการทดสอบสำหรับเมธอด 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") } }

คุณต้องการทดสอบว่าตัวแปร registerEnabled จะถูกตั้งค่าเป็นเท็จหรือไม่ หลังจากที่วิธีการของเราแจ้งให้คุณทราบว่าไม่มีอีเมล (ผู้ใช้รายอื่นใช้ไปแล้ว)

ถ้าคุณทำการทดสอบนี้ มันจะผ่าน แต่ลองอีกสิ่งหนึ่ง เปลี่ยนคำยืนยันของคุณเป็น:

 XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")

หากคุณเรียกใช้การทดสอบอีกครั้ง การทดสอบจะผ่านอีกครั้ง

นี่เป็นเพราะคำยืนยันของเราไม่ได้ถูกยืนยันด้วยซ้ำ การทดสอบหน่วยสิ้นสุดลงก่อนที่จะดำเนินการบล็อกการโทรกลับ (โปรดจำไว้ว่าในการใช้งานบริการเครือข่ายจำลองของเรา ตั้งค่าให้รอหนึ่งวินาทีก่อนที่จะกลับมา)

โชคดีที่ด้วย Xcode 6 Apple ได้เพิ่มความคาดหวังในการทดสอบให้กับเฟรมเวิร์ก XCTest เป็นคลาส XCTestExpectation คลาส XCTestExpectation ทำงานดังนี้:

  1. ในตอนเริ่มต้นของการทดสอบ คุณต้องตั้ง ความคาดหวัง ในการทดสอบ - ด้วยข้อความง่ายๆ ที่อธิบายสิ่งที่คุณคาดหวังจากการทดสอบ
  2. ในบล็อก async หลังจากรันโค้ดทดสอบของคุณแล้ว คุณจะทำตามความคาดหวังได้
  3. เมื่อสิ้นสุดการทดสอบ คุณต้องตั้งค่าบล็อก waitForExpectationWithTimer มันจะดำเนินการเมื่อเป็นไปตามความคาดหวังหรือถ้าหมดเวลา - แล้วแต่ว่าจะเกิดอะไรขึ้นก่อน
  4. ตอนนี้ การทดสอบหน่วยจะไม่สิ้นสุดจนกว่าความคาดหวังจะสำเร็จหรือจนกว่าตัวจับเวลาการคาดคะเนจะหมด

มาเขียนการทดสอบของเราใหม่เพื่อใช้คลาส 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") } } }

หากคุณทำการทดสอบตอนนี้ มันจะล้มเหลว - อย่างที่ควรจะเป็น มาแก้ไขการทดสอบเพื่อให้ผ่าน เปลี่ยนคำยืนยันเป็น:

 XCTAssert(self.registrationVM.registrationEnabled.value == false, "Email address is not available, registration should be disabled")

ทำการทดสอบอีกครั้งเพื่อให้ผ่าน คุณสามารถลองเปลี่ยนเวลาหน่วงในการดำเนินการจำลองบริการเครือข่ายเพื่อดูว่าจะเกิดอะไรขึ้นหากตัวจับเวลาที่คาดหวังหมดลง

วิธีการทดสอบด้วยการโทรแบบอะซิงโครนัสโดยไม่ต้องโทรกลับ

ตัวอย่างวิธีโครงการของเรา attemptUserRegistration ใช้เมธอด NetworkService.attemptRegistration ที่มีโค้ดที่ทำงานแบบอะซิงโครนัส เมธอดพยายามลงทะเบียนผู้ใช้ด้วยบริการแบ็กเอนด์

ในแอปพลิเคชันสาธิตของเรา วิธีการจะรอเพียงหนึ่งวินาทีเพื่อจำลองการโทรผ่านเครือข่าย และปลอมลงทะเบียนสำเร็จ หากการลงทะเบียนสำเร็จ ค่า loginSuccessful จะถูกตั้งค่าเป็นจริง มาทำการทดสอบหน่วยเพื่อตรวจสอบพฤติกรรมนี้กัน

 func testAttemptRegistration() { registrationVM.emailAddress = "[email protected]" registrationVM.password = "123456" registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, "Login must be successful") }

หากรัน การทดสอบนี้จะล้มเหลวเนื่องจากค่า loginSuccessful จะไม่ถูกตั้งค่าเป็น true จนกว่าเมธอด networkService.attemptRegistration แบบอะซิงโครนัสจะเสร็จสิ้น

เนื่องจากคุณได้สร้าง NetworkServiceImpl จำลองขึ้นโดยที่เมธอด attemptRegistration จะรอหนึ่งวินาทีก่อนที่จะกลับมาลงทะเบียนให้สำเร็จ คุณสามารถใช้ Grand Central Dispatch (GCD) และใช้เมธอด asyncAfter เพื่อตรวจสอบการยืนยันหลังจากผ่านไปหนึ่งวินาที หลังจากเพิ่ม asyncAfter ของ GCD หลังจากโค้ดทดสอบของเราจะมีลักษณะดังนี้:

 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") } }

หากคุณให้ความสนใจ คุณจะรู้ว่าวิธีนี้ยังคงใช้ไม่ได้เพราะวิธีการทดสอบจะดำเนินการ ก่อนที่ จะดำเนินการบล็อก asyncAfter และวิธีการนั้นจะผ่านเป็นผลสำเร็จ เสมอ โชคดีที่มีคลาส XCTestException

มาเขียนวิธีการของเราใหม่เพื่อใช้คลาส 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") } } }

ด้วยการทดสอบหน่วยที่ครอบคลุม RegistrationViewModel ของเรา คุณจะมั่นใจมากขึ้นว่าการเพิ่มฟังก์ชันการทำงานใหม่หรือการอัปเดตที่มีอยู่จะไม่ทำให้เกิดปัญหาใดๆ

หมายเหตุสำคัญ: การทดสอบหน่วยจะสูญเสียค่าไป หากไม่มีการอัปเดตเมื่อฟังก์ชันการทำงานของเมธอดที่ครอบคลุมมีการเปลี่ยนแปลง การทดสอบหน่วยการเขียนเป็นกระบวนการที่ต้องติดตามส่วนที่เหลือของแอปพลิเคชัน

เคล็ดลับ: อย่าเลื่อนการสอบข้อเขียนจนจบ เขียนแบบทดสอบขณะพัฒนา วิธีนี้จะทำให้คุณมีความเข้าใจที่ดีขึ้นเกี่ยวกับสิ่งที่ต้องได้รับการทดสอบและกรณีชายแดนคืออะไร

การเขียนแบบทดสอบ UI

หลังจากที่การทดสอบหน่วยทั้งหมดได้รับการพัฒนาและดำเนินการสำเร็จอย่างสมบูรณ์แล้ว คุณสามารถมั่นใจได้ว่าโค้ดแต่ละหน่วยทำงานอย่างถูกต้อง แต่หมายความว่าแอปพลิเคชันโดยรวมของคุณทำงานได้ตามที่ตั้งใจไว้หรือไม่

นั่นคือที่มาของการทดสอบการรวม ซึ่งการทดสอบ UI เป็นองค์ประกอบที่สำคัญ

ก่อนเริ่มการทดสอบ UI จำเป็นต้องมีองค์ประกอบ UI และการโต้ตอบ (หรือเรื่องราวของผู้ใช้) ก่อนจึงจะทดสอบได้ มาสร้างมุมมองที่เรียบง่ายและตัวควบคุมมุมมองกัน

  1. เปิด Main.storyboard และสร้างตัวควบคุมมุมมองแบบง่ายที่จะมีลักษณะเหมือนในภาพด้านล่าง

ภาพ: การสร้างมุมมองที่เรียบง่ายและตัวควบคุมมุมมอง

ตั้งค่าแท็กฟิลด์ข้อความอีเมลเป็น 100 แท็กฟิลด์ข้อความรหัสผ่านเป็น 101 และแท็กยืนยันรหัสผ่านเป็น 102

  1. เพิ่มไฟล์ตัวควบคุมมุมมองใหม่ RegistrationViewController.swift และเชื่อมต่อช่องทางทั้งหมดกับกระดานเรื่องราว
 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() } }

ที่นี่คุณกำลังเพิ่ม IBOutlets และโครงสร้าง TextFieldTags ให้กับคลาส

ซึ่งจะทำให้คุณสามารถระบุฟิลด์ข้อความที่กำลังแก้ไขได้ ในการใช้คุณสมบัติไดนามิกในโมเดลการดู คุณต้อง 'ผูก' คุณสมบัติไดนามิกในตัวควบคุมมุมมอง คุณสามารถทำได้ในเมธอด bindViewModel :

 fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = $0 } } }

มาเพิ่มวิธีการมอบสิทธิ์ฟิลด์ข้อความเพื่อติดตามว่าฟิลด์ข้อความใดกำลังได้รับการอัปเดต:

 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 }
  1. อัปเดต AppDelegate เพื่อผูกตัวควบคุมมุมมองกับโมเดลมุมมองที่เหมาะสม (โปรดทราบว่าขั้นตอนนี้เป็นข้อกำหนดของสถาปัตยกรรม MVVM) รหัส AppDelegate ที่อัปเดตแล้วควรมีลักษณะดังนี้:
 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 } }

ไฟล์สตอรีบอร์ดและ RegistrationViewController นั้นเรียบง่ายมาก แต่ก็เพียงพอที่จะแสดงให้เห็นว่าการทดสอบ UI อัตโนมัติทำงานอย่างไร

หากทุกอย่างได้รับการตั้งค่าอย่างถูกต้อง ปุ่มลงทะเบียนควรปิดใช้งานเมื่อแอปเริ่มทำงาน เมื่อใดและเฉพาะเมื่อกรอกข้อมูลและถูกต้องทุกช่องแล้ว ควรเปิดใช้งานปุ่มลงทะเบียน

เมื่อตั้งค่านี้แล้ว คุณสามารถสร้างการทดสอบ UI ครั้งแรกได้

การทดสอบ UI ของเราควรตรวจสอบว่าปุ่มลงทะเบียนจะเปิดใช้งานหรือไม่ หากมีการป้อนที่อยู่อีเมลที่ถูกต้อง รหัสผ่านที่ถูกต้อง และการยืนยันรหัสผ่านที่ถูกต้องเท่านั้น วิธีตั้งค่ามีดังนี้

  1. เปิดไฟล์ TestingIOSUITests.swift
  2. ลบ testExample() และเพิ่ม testRegistrationButtonEnabled()
  3. วางเคอร์เซอร์ในเมธอด testRegistrationButtonEnabled เหมือนกับว่าคุณกำลังจะเขียนบางอย่างที่นั่น
  4. กดปุ่มทดสอบบันทึก UI (วงกลมสีแดงที่ด้านล่างของหน้าจอ)

ภาพ: ภาพหน้าจอแสดงปุ่มทดสอบ Record UI

  1. เมื่อกดปุ่มบันทึก โปรแกรมจะเปิดขึ้น
  2. หลังจากเปิดตัวแอปพลิเคชัน ให้แตะในช่องข้อความอีเมลและเขียน '[email protected]' คุณจะสังเกตเห็นว่ารหัสปรากฏขึ้นโดยอัตโนมัติภายในเนื้อหาของวิธีการทดสอบ

คุณสามารถบันทึกคำสั่ง UI ทั้งหมดได้โดยใช้คุณสมบัตินี้ แต่คุณอาจพบว่าการเขียนคำสั่งง่ายๆ ด้วยตนเองจะเร็วกว่ามาก

นี่คือตัวอย่างคำสั่งเครื่องบันทึกสำหรับการแตะที่ช่องข้อความรหัสผ่านและป้อนที่อยู่อีเมล '[email protected]'

 let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]")
  1. หลังจากที่บันทึกการโต้ตอบ UI ที่คุณต้องการทดสอบแล้ว ให้กดปุ่มหยุดอีกครั้ง (ป้ายกำกับปุ่มบันทึกเปลี่ยนเป็นหยุดเมื่อคุณเริ่มบันทึก) เพื่อหยุดการบันทึก
  2. หลังจากที่คุณมีตัวบันทึกการโต้ตอบ UI แล้ว คุณสามารถเพิ่ม XCTAsserts ต่างๆ เพื่อทดสอบสถานะต่างๆ ของแอปพลิเคชันหรือองค์ประกอบ UI ได้

ภาพ: แอนิเมชั่นแสดงคำสั่งเครื่องบันทึกสำหรับการแตะที่ฟิลด์รหัสผ่าน

คำแนะนำที่บันทึกไว้ไม่ได้อธิบายด้วยตนเองเสมอไป และอาจทำให้วิธีการทดสอบทั้งหมดอ่านและเข้าใจยากขึ้นเล็กน้อย โชคดีที่คุณสามารถป้อนคำสั่ง UI ได้ด้วยตนเอง

Let's create the following UI instructions manually:

  1. User taps on the password text field.
  2. 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.