كيفية كتابة الاختبارات الآلية لنظام iOS
نشرت: 2022-03-11بصفتك مطورًا جيدًا ، فأنت تبذل قصارى جهدك لاختبار جميع الوظائف وكل مسار ونتائج الكود الممكنة في البرنامج الذي تكتبه. ولكن من النادر جدًا وغير المعتاد أن تتمكن من اختبار كل نتيجة محتملة يدويًا وكل مسار ممكن قد يسلكه المستخدم.
مع زيادة حجم التطبيق وزيادة تعقيده ، تزداد احتمالية فقدان شيء ما من خلال الاختبار اليدوي بشكل كبير.
سيجعلك الاختبار الآلي ، لكل من واجهة المستخدم وواجهة برمجة التطبيقات للخدمة الخلفية ، أكثر ثقة في أن كل شيء يعمل على النحو المنشود وسيقلل من الضغط عند التطوير أو إعادة البناء أو إضافة ميزات جديدة أو تغيير الميزات الحالية.
باستخدام الاختبارات الآلية ، يمكنك:
- تقليل الأخطاء: لا توجد طريقة من شأنها أن تزيل تمامًا أي احتمال لوجود أخطاء في التعليمات البرمجية الخاصة بك ، ولكن الاختبارات الآلية يمكن أن تقلل بشكل كبير من عدد الأخطاء.
- قم بإجراء التغييرات بثقة: تجنب الأخطاء عند إضافة ميزات جديدة ، مما يعني أنه يمكنك إجراء التغييرات بسرعة ودون عناء.
- توثيق الكود الخاص بنا: عند البحث في الاختبارات ، يمكننا أن نرى بوضوح ما هو متوقع من وظائف معينة ، وما هي الشروط ، وما هي الحالات الجانبية.
- إعادة البناء دون ألم: بصفتك مطورًا ، قد تخاف أحيانًا من إعادة البناء ، خاصة إذا كنت بحاجة إلى إعادة بناء جزء كبير من التعليمات البرمجية. اختبارات الوحدة هنا للتأكد من أن الكود المعاد بناءه لا يزال يعمل على النحو المنشود.
تعلمك هذه المقالة كيفية هيكلة وتنفيذ الاختبار الآلي على نظام iOS الأساسي.
اختبارات الوحدة مقابل اختبارات واجهة المستخدم
من المهم التفريق بين اختبارات الوحدة وواجهة المستخدم.
يختبر اختبار الوحدة وظيفة معينة في سياق محدد . تتحقق اختبارات الوحدة من أن الجزء الذي تم اختباره من الكود (عادةً ما يكون وظيفة واحدة) يقوم بما يفترض القيام به. هناك الكثير من الكتب والمقالات حول اختبارات الوحدة ، لذلك لن نغطي ذلك في هذا المنشور.
اختبارات واجهة المستخدم مخصصة لاختبار واجهة المستخدم. على سبيل المثال ، يتيح لك اختبار ما إذا تم تحديث طريقة عرض على النحو المنشود أو تشغيل إجراء معين كما ينبغي عندما يتفاعل المستخدم مع عنصر واجهة مستخدم معين.
يختبر كل اختبار واجهة مستخدم تفاعل مستخدم معين مع واجهة مستخدم التطبيق. يمكن وينبغي إجراء الاختبار الآلي على مستوى كل من اختبار الوحدة واختبار واجهة المستخدم.
إعداد الاختبارات الآلية
نظرًا لأن XCode يدعم اختبار الوحدة وواجهة المستخدم خارج الصندوق ، فمن السهل والمباشر إضافتهما إلى مشروعك. عند إنشاء مشروع جديد ، حدد ببساطة "تضمين اختبارات الوحدة" و "تضمين اختبارات واجهة المستخدم".
عند إنشاء المشروع ، ستتم إضافة هدفين جديدين إلى مشروعك عند التحقق من هذين الخيارين. أسماء الأهداف الجديدة تحتوي على "اختبارات" أو "اختبارات UITests" في نهاية الاسم.
هذا هو. أنت جاهز لكتابة اختبارات آلية لمشروعك.
إذا كان لديك بالفعل مشروع حالي وترغب في إضافة دعم اختبارات الوحدة وواجهة المستخدم ، فسيتعين عليك القيام بالمزيد من العمل ، ولكنه أيضًا بسيط ومباشر للغاية.
انتقل إلى File → New → Target وحدد iOS Unit Testing Bundle لاختبارات الوحدة أو حزمة اختبار واجهة مستخدم iOS لاختبارات واجهة المستخدم.
اضغط على التالي .
في شاشة خيارات الهدف ، يمكنك ترك كل شيء كما هو (إذا كان لديك أهداف متعددة وترغب في اختبار أهداف محددة فقط ، فحدد الهدف في القائمة المنسدلة الهدف المراد اختباره).
اضغط على إنهاء . كرر هذه الخطوة لاختبارات واجهة المستخدم ، وسيكون لديك كل شيء جاهزًا لبدء كتابة الاختبارات الآلية في مشروعك الحالي.
اختبارات وحدة الكتابة
قبل أن نبدأ في كتابة اختبارات الوحدة ، يجب أن نفهم تشريحهم. عندما تقوم بتضمين اختبارات الوحدة في مشروعك ، سيتم إنشاء نموذج اختبار. في حالتنا ، سيبدو كما يلي:
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 خارج نطاق هذه المقالة ، ولكن يمكنك قراءة المزيد عنها في هذه المقالة.
لنقم بإنشاء فئة طراز العرض المسؤولة عن تسجيل المستخدم. .
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>
على true ، ستعمل وحدة التحكم في العرض المرتبطة (المتصلة) بـ RegistrationViewModel
على تمكين زر التسجيل. عند تعيين loginSuccessful
على "صحيح" ، سيتم تحديث طريقة العرض المتصلة نفسها.
دعنا الآن نضيف بعض الطرق للتحقق من صحة كلمة المرور وتنسيق البريد الإلكتروني.
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
الديناميكية.
للحفاظ على المثال بسيطًا ، أضف طريقتين بسيطتين - واحدة للتحقق من توفر البريد الإلكتروني والأخرى لمحاولة التسجيل باستخدام اسم المستخدم وكلمة المرور المحددين.
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 كبروتوكول وفئة التنفيذ الخاصة به.
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) }) } }
كلتا الطريقتين تنتظران بعض الوقت (تزوير التأخير الزمني لطلب الشبكة) ثم استدعاء طرق رد الاتصال المناسبة.
نصيحة: من الممارسات الجيدة استخدام البروتوكولات (المعروفة أيضًا باسم الواجهات في لغات البرمجة الأخرى). يمكنك قراءة المزيد عنها إذا كنت تبحث عن "مبدأ البرمجة للواجهات". سترى أيضًا كيف يعمل بشكل جيد مع اختبار الوحدة.
الآن ، عند تعيين مثال ، يمكننا كتابة اختبارات وحدة لتغطية طرق هذه الفئة.
قم بإنشاء فئة اختبار جديدة لنموذج العرض الخاص بنا. انقر بزر الماوس الأيمن فوق مجلد
TestingIOSTests
في جزء Project Navigator ، وحدد New File → Unit Test Case Class ، وقم بتسميتهRegistrationViewModelTests
.احذف
testExample
وtestPerformanceExample
، لأننا نريد إنشاء طرق الاختبار الخاصة بنا.نظرًا لأن Swift تستخدم وحدات واختباراتنا في وحدة نمطية مختلفة عن كود التطبيق الخاص بنا ، يتعين علينا استيراد وحدة التطبيق الخاصة بنا على أنها
@testable
. أسفل بيان الاستيراد وتعريف الفئة ، أضف@testable import TestingIOS
(أو اسم الوحدة النمطية للتطبيق الخاص بك). بدون هذا ، لن نتمكن من الرجوع إلى أي من فئات أو طرق تطبيقنا.أضف متغير
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: أنت تقوم بإنشاء مثيل لـ VM للتسجيل في نص أسلوب الاختبار
هناك setUp
و tearDown
لسبب ما.
تُستخدم هذه الطرق لبدء أو إعداد جميع الكائنات المطلوبة في الاختبار. يجب عليك استخدام هذه الطرق لتجنب تكرار الكود عن طريق كتابة نفس طرق الإعداد أو init في كل طريقة اختبار. لا يعد عدم استخدام طرق الإعداد والتمزيق مشكلة كبيرة دائمًا ، خاصةً إذا كان لديك تكوين محدد بالفعل لطريقة اختبار معينة.
نظرًا لأن التهيئة الخاصة بنا لفئة RegistrationViewModel
بسيطة جدًا ، فسوف تقوم بإعادة تشكيل فئة الاختبار الخاصة بك لاستخدام طرق الإعداد والتمزيق.
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: لديك تأكيدات متعددة في طريقة اختبار واحدة
على الرغم من أن هذه ليست مشكلة كبيرة ، إلا أن هناك بعض المدافعين عن وجود تأكيد واحد لكل طريقة.
السبب الرئيسي لهذا المبدأ هو اكتشاف الخطأ.
إذا كانت إحدى طرق الاختبار تحتوي على تأكيدات متعددة وفشلت الطريقة الأولى ، فسيتم تمييز طريقة الاختبار بأكملها على أنها فاشلة. لن يتم اختبار التأكيدات الأخرى.
بهذه الطريقة ستكتشف خطأ واحدًا فقط في كل مرة. لن تعرف ما إذا كانت التأكيدات الأخرى ستفشل أو تنجح.
ليس من السيئ دائمًا أن يكون لديك تأكيدات متعددة في طريقة واحدة لأنه يمكنك فقط إصلاح خطأ واحد في كل مرة ، لذلك قد لا يمثل اكتشاف خطأ واحد في كل مرة مشكلة كبيرة.

في حالتنا ، يتم اختبار صلاحية تنسيق البريد الإلكتروني. نظرًا لأن هذه وظيفة واحدة فقط ، فقد يكون من المنطقي تجميع كل التأكيدات معًا في طريقة واحدة لتسهيل قراءة الاختبار وفهمه.
نظرًا لأن هذه المشكلة ليست في الواقع مشكلة كبيرة وقد يجادل البعض بأنها ليست مشكلة على الإطلاق ، فسوف تحافظ على طريقة الاختبار كما هي.
عندما تكتب اختبارات الوحدة الخاصة بك ، فالأمر متروك لك لتحديد المسار الذي تريد أن تسلكه لكل طريقة اختبار. على الأرجح ، ستجد أن هناك أماكن يؤكد فيها الفرد لكل اختبار أن الفلسفة منطقية ، وأخرى لا يكون فيها ذلك منطقيًا.
طرق الاختبار مع المكالمات غير المتزامنة
بغض النظر عن مدى بساطة التطبيق ، هناك فرصة كبيرة لوجود طريقة يجب تنفيذها على مؤشر ترابط آخر بشكل غير متزامن ، خاصة وأنك تحب عادةً أن يكون لديك واجهة المستخدم قيد التنفيذ في سلسلة المحادثات الخاصة بها ..
تكمن المشكلة الرئيسية في اختبار الوحدة والمكالمات غير المتزامنة في أن المكالمة غير المتزامنة تستغرق وقتًا حتى تنتهي ، لكن اختبار الوحدة لن ينتظر حتى ينتهي. نظرًا لانتهاء اختبار الوحدة قبل تنفيذ أي كود داخل كتلة غير متزامنة ، سينتهي اختبارنا دائمًا بنفس النتيجة (بغض النظر عما تكتبه في الكتلة غير المتزامنة).
لتوضيح ذلك ، دعنا ننشئ اختبارًا لطريقة 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 سيتم تعيينه على false بعد أن تخبرك طريقتنا أن البريد الإلكتروني غير متاح (تم استخدامه بالفعل بواسطة مستخدم آخر).
إذا قمت بإجراء هذا الاختبار ، فسوف ينجح. لكن فقط جرب شيئًا آخر. غيّر تأكيدك إلى:
XCTAssert(self.registrationVM.registrationEnabled.value == true, "Email address is not available, registration should be disabled")
إذا قمت بإجراء الاختبار مرة أخرى ، فسيتم اجتيازه مرة أخرى.
هذا لأن تأكيدنا لم يتم تأكيده. انتهى اختبار الوحدة قبل تنفيذ كتلة رد الاتصال (تذكر ، في تنفيذ خدمة الشبكة السخرية لدينا ، تم ضبطها على الانتظار لمدة ثانية واحدة قبل أن تعود).
لحسن الحظ ، مع Xcode 6 ، أضافت Apple توقعات الاختبار إلى إطار XCTest باعتبارها فئة XCTestExpectation
. تعمل فئة XCTestExpectation
:
- في بداية الاختبار ، حددت توقعاتك للاختبار - بنص بسيط يصف ما توقعته من الاختبار.
- في كتلة غير متزامنة بعد تنفيذ كود الاختبار الخاص بك ، فإنك تحقق التوقعات.
- في نهاية الاختبار ، تحتاج إلى تعيين كتلة
waitForExpectationWithTimer
. سيتم تنفيذه عند تحقيق التوقع أو في حالة نفاد العداد - أيهما يحدث أولاً. - الآن ، لن ينتهي اختبار الوحدة حتى يتم تحقيق التوقع أو حتى نفاد مؤقت التوقع.
دعنا نعيد كتابة اختبارنا لاستخدام فئة 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
هذا الاختبار لأن قيمة تسجيل الدخول الناجحة لن يتم تعيينها على "صحيح" حتى تنتهي طريقة networkService.attemptRegistration
غير المتزامنة.
نظرًا لأنك أنشأت NetworkServiceImpl
مزعجًا حيث ستنتظر طريقة attemptRegistration
لمدة ثانية واحدة قبل إعادة تسجيل ناجح ، يمكنك فقط استخدام Grand Central Dispatch (GCD) ، واستخدام طريقة asyncAfter
للتحقق من تأكيدك بعد ثانية واحدة. بعد إضافة غير المتزامن لـ GCD ، asyncAfter
رمز الاختبار كما يلي:
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
الخاص بنا ، يمكنك الآن أن تكون أكثر ثقة في أن إضافة وظائف جديدة أو تحديث حالية لن تؤدي إلى كسر أي شيء.
ملاحظة مهمة: ستفقد اختبارات الوحدة قيمتها إذا لم يتم تحديثها عندما تتغير وظائف الطرق التي تغطيها. اختبارات الوحدة الكتابية هي عملية يجب أن تواكب بقية التطبيق.
نصيحة: لا تؤجل كتابة الاختبارات حتى النهاية. اكتب الاختبارات أثناء التطوير. بهذه الطريقة سيكون لديك فهم أفضل لما يجب اختباره وما هي الحالات الحدودية.
كتابة اختبارات واجهة المستخدم
بعد تطوير جميع اختبارات الوحدة وتنفيذها بنجاح ، يمكنك أن تكون واثقًا تمامًا من أن كل وحدة من الكود تعمل بشكل صحيح ، ولكن هل هذا يعني أن تطبيقك ككل يعمل على النحو المنشود؟
هذا هو المكان الذي تأتي فيه اختبارات التكامل ، والتي تعد اختبارات واجهة المستخدم عنصرًا أساسيًا فيها.
قبل البدء في اختبار واجهة المستخدم ، يجب أن تكون هناك بعض عناصر واجهة المستخدم والتفاعلات (أو قصص المستخدم) لاختبارها. دعونا ننشئ عرضًا بسيطًا ووحدة تحكم العرض الخاصة به.
- افتح
Main.storyboard
وأنشئ وحدة تحكم عرض بسيطة ستبدو مثل تلك الموجودة في الصورة أدناه.
قم بتعيين علامة حقل نص البريد الإلكتروني على 100 ، وعلامة حقل نص كلمة المرور على 101 ، وعلامة تأكيد كلمة المرور على 102.
- أضف ملف وحدة تحكم عرض جديد
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 }
- قم بتحديث
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
أمرًا بسيطًا حقًا ، لكنهما كافيان لتوضيح كيفية عمل اختبار واجهة المستخدم الآلي.
إذا تم إعداد كل شيء بشكل صحيح ، فيجب تعطيل زر التسجيل عند بدء تشغيل التطبيق. عندما ، وفقط عندما ، يتم ملء جميع الحقول وصالحة ، يجب تمكين زر التسجيل.
بمجرد إعداد هذا ، يمكنك إنشاء أول اختبار لواجهة المستخدم.
يجب أن يتحقق اختبار واجهة المستخدم الخاص بنا مما إذا كان زر التسجيل سيصبح ممكّنًا فقط إذا تم إدخال عنوان بريد إلكتروني صالح وكلمة مرور صالحة وتأكيد كلمة مرور صالحة. فيما يلي كيفية إعداد هذا:
- افتح ملف
TestingIOSUITests.swift
. - احذف أسلوب
testExample()
وأضف طريقةtestRegistrationButtonEnabled()
. - ضع المؤشر في طريقة
testRegistrationButtonEnabled
كما لو كنت ستكتب شيئًا هناك. - اضغط على زر اختبار واجهة المستخدم الخاصة بالتسجيل (الدائرة الحمراء أسفل الشاشة).
- عند الضغط على زر التسجيل ، سيتم تشغيل التطبيق
- بعد تشغيل التطبيق ، انقر فوق حقل نص البريد الإلكتروني واكتب "[email protected]". ستلاحظ أن الرمز يظهر تلقائيًا داخل جسم طريقة الاختبار.
يمكنك تسجيل جميع تعليمات واجهة المستخدم باستخدام هذه الميزة ، ولكن قد تجد أن كتابة التعليمات البسيطة يدويًا ستكون أسرع بكثير.
هذا مثال لتعليمات المُسجل للنقر على حقل نص كلمة المرور وإدخال عنوان بريد إلكتروني "[email protected]"
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:"Email Address").children(matching: .textField).element emailTextField.tap() emailTextField.typeText("[email protected]")
- بعد تسجيل تفاعلات واجهة المستخدم التي تريد اختبارها ، اضغط على زر الإيقاف مرة أخرى (تم تغيير تسمية زر التسجيل للتوقف عند بدء التسجيل) لإيقاف التسجيل.
- بعد أن يكون لديك مسجل تفاعلات واجهة المستخدم ، يمكنك الآن إضافة
XCTAsserts
المختلفة لاختبار الحالات المختلفة للتطبيق أو عناصر واجهة المستخدم.
لا تكون التعليمات المسجلة دائمًا تشرح نفسها بنفسها وقد تجعل طريقة الاختبار بأكملها صعبة القراءة والفهم. لحسن الحظ ، يمكنك إدخال تعليمات واجهة المستخدم يدويًا.
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.