توليد mock classes لاختبارات الوحدة في Swift
استخدام الـ mocks في اختبارات الوحدة يسهّل كتابة حالات الاختبار دون إعداد التبعيات الفعلية. لكن كتابة كل mock classes يدويًا وتحديثها مع كل تغيير أمر مرهق. وهنا يساعد Sourcery على توليدها تلقائيًا بأمر واحد.

تُكتب البرمجيات الحديثة وفق نموذج البرمجة كائنية التوجه، فتتكون من كائنات متعددة لكل منها غرض محدد. وأساس اختبار هذه الكائنات هو اختبار الوحدة (unit testing). تُكتب حالة اختبار الوحدة بالتحقق من الآثار المطلوبة بعد تنفيذ عملية على كائن ما. ولاختبار هذه الكائنات بفعالية، عليكم اتباع خطوات معينة لضمان الإعداد الصحيح والأداء السليم.
لنفكر في وحدة لتطبيق عدّاد فيه زر للزيادة. نحتاج إلى كائن يتولى العدّ ويتتبعه. إنه جوهر التطبيق، ويضم منطق الأعمال الرئيسي.
protocol CounterDelegate: AnyObject {
func counter(_ counter: Counter, didUpdateCount count: Int)
}
class Counter {
weak var delegate: CounterDelegate?
private(set) var count = 0 {
didSet { delegate?.counter(self, didUpdateCount: count) }
}
func increment() {
count = 1
}
}
يحتوي الـ class المسمى Counter على خاصية عددية لتخزين الحالة، ودالة لزيادتها. عند استدعاء الدالة increment نتوقع أن تحدّث الخاصية count وأن تُبلغ الـ delegate بالتغيير. التحقق من قيمة count سهل، إذ يكفي فحص المساواة.
@testable import Tracker
import XCTest
final class CounterTests: XCTestCase {
private var counter: Counter!
override func setUp() {
super.setUp()
counter = Counter()
}
func testIncrement() {
counter.increment()
XCTAssertEqual(counter.count, 1)
}
}
لكن اختبار استدعاء الـ delegate أصعب قليلًا. لتتبع استدعاءات الدوال والتحقق منها، نحتاج إلى تعيين كائن mock في الخاصية delegate. وبما أن Swift لغة ذات أنواع ثابتة (static-typed)، فلا بد من تنفيذ فعلي للـ mock كي يُترجم الكود. وكتابة كل تلك الخصائص والدوال يدويًا عرضة للأخطاء وتستهلك الوقت، لذا نحتاج إلى أداة تساعدنا في هذه المهمة. ويمكن استخدام أدوات مثل Sourcery لتوليد كائنات الـ mock هذه بكفاءة.
فهم كائنات الـ Mock
كائنات الـ mock جزء أساسي من اختبارات الوحدة، إذ تتيح للمطورين عزل التبعيات واختبار مكونات محددة من الكود بمعزل عن غيرها. وفي سياق تطوير Swift، يمكن استخدامها لمحاكاة سلوك الكائنات الحقيقية، مما يسهّل اختبار الأنظمة المعقدة. وباستخدامها، يضمن المطورون أن يتصرف الكود كما هو متوقع، حتى حين تكون التبعيات غير متاحة أو صعبة الاختبار. ويساعد هذا العزل على اكتشاف المشكلات داخل الوحدة المختبرة دون تدخل عوامل خارجية، مما ينتج كودًا أكثر موثوقية وأسهل صيانة.
التعرّف على Sourcery
يولّد Sourcery الكود المتكرر (boilerplate) لأغراض متعددة عبر تحليل ملفات Swift. ومن استخداماته إنشاء mock class تلقائيًا. لإنتاج الـ class المسمى CounterDelegateMock للـ protocol المسمى CounterDelegate:
-
ثبّتوا Sourcery باتباع [التعليمات
](https://github.com/krzysztofzablocki/Sourcery#installation)
-
نزّلوا [AutoMockable.stencil
](https://github.com/krzysztofzablocki/Sourcery/blob/master/Templates/Templates/AutoMockable.stencil)
-
علّموا الـ protocol بأنه auto-mockable بإضافة تعليق (ويمكننا أيضًا جعله يلتزم بالـ protocol المسمى AutoMockable)
// sourcery: AutoMockable
protocol CounterDelegate: AnyObject {
func counter(_ counter: Counter, didUpdateCount count: Int)
}
يُنتج تنفيذ الأمر التالي ملفًا اسمه AutoMockable.generated.swift يحتوي على CounterDelegateMock:
sourcery --sources <sources path> --templates <templates path> --output <output path>
class CounterDelegateMock: CounterDelegate {
//MARK: - counter
var counterDidUpdateCountCallsCount = 0
var counterDidUpdateCountCalled: Bool {
return counterDidUpdateCountCallsCount > 0
}
var counterDidUpdateCountReceivedArguments: (counter: Counter, count: Int)?
var counterDidUpdateCountReceivedInvocations: [(counter: Counter, count: Int)] = []
var counterDidUpdateCountClosure: ((Counter, Int) -> Void)?
func counter(_ counter: Counter, didUpdateCount count: Int) {
counterDidUpdateCountCallsCount += 1
counterDidUpdateCountReceivedArguments = (counter: counter, count: count)
counterDidUpdateCountReceivedInvocations.append((counter: counter, count: count))
counterDidUpdateCountClosure?(counter, count)
}
}
ينفّذ Sourcery دوال mock مفصلة تلقائيًا، مما يتيح كتابة حالات اختبار متقدمة. فإلى جانب التحقق من استدعاء الدالة أو عدمه، يفيد التحقق من عدد مرات الاستدعاء والوسائط المستلمة في اكتشاف الأخطاء أو منع التراجعات (regressions).
@testable import Tracker
import XCTest
final class CounterTests: XCTestCase {
private var delegate: CounterDelegateMock!
private var counter: Counter!
override func setUp() {
super.setUp()
delegate = CounterDelegateMock()
counter = Counter()
counter.delegate = delegate
}
func testIncrement() {
counter.increment()
XCTAssertEqual(counter.count, 1)
// Assert counter(_:, didUpdateCount:) is called
XCTAssertTrue(delegate.counterDidUpdateCountCalled)
// Assert received count parameter to be 1
XCTAssertEqual(delegate.counterDidUpdateCountReceivedArguments?.count, 1)
}
}
أصبحت لدينا الآن مجموعة كاملة من class وdelegate protocol وmock delegate class، تشكّل وحدة مختبرة جيدًا. وطوال العملية ركزنا فقط على منطق الأعمال الفعلي، واستفدنا من الكود المولّد تلقائيًا الذي يفوق ما كتبناه يدويًا. وهذا يعني إنتاجية أعلى وبرمجيات أفضل.
مزايا استخدام Sourcery لتوليد mock classes
- توفير الوقت بتقليل الكتابة المتكررة: دوال الـ mock متشابهة الشكل عادةً، من عدد الاستدعاءات إلى الوسائط المستلمة والقيمة المُرجعة. لذا من الأفضل أتمتة هذه المهام المتسقة والمتكررة.
- صيانة أسهل: يمكن توليد الـ mocks بأمر واحد بسرعة كبيرة. ويكفي أمر واحد بعد إعادة الهيكلة (refactoring) لإبقاء الـ mock class محدّثًا.
- معمارية برمجيات أفضل: اختبارات الوحدة الأسهل والأكثر تدفعنا إلى كتابة برمجيات أفضل، مقسمة إلى مكونات أصغر ومختبرة جيدًا.
قيود Sourcery في توليد mock classes1. لا يدعم جميع أنواع الدوال: الدوال التي تستخدم أنواعًا عامة (generic types) غير متوافقة مع Sourcery.
- لا يميّز بين الدوال المحمّلة (overloaded): بما أن Sourcery يحذف أسماء المعاملات، تسبب الدوال المحمّلة مشكلة تكرار الرموز (duplicated symbol). 3. استيراد الـ modules يدويًا: حاليًا يجب استيراد الـ modules يدويًا، لأن Sourcery يحلل بنية الكود (parse) فقط دون تحليل دلالته.
التوليد التلقائي لـ Mock Classes
التوليد التلقائي لـ mock classes ميزة قوية في Sourcery، تتيح للمطورين توفير الوقت والجهد عند إنشاء كائنات الـ mock. فباستخدام قالب AutoMockable في Sourcery، يستطيع المطورون توليد mock classes للـ protocols تلقائيًا، مما يسهّل كتابة اختبارات وحدة شاملة. وتفيد هذه الميزة خصوصًا في الأنظمة المعقدة، حيث يستهلك إنشاء كائنات الـ mock يدويًا الوقت ويكون عرضة للأخطاء. مع Sourcery، يمكنكم التركيز على منطق الأعمال الفعلي وترك المهمة المتكررة والمملة لكتابة mock classes للأداة، مما يضمن الاتساق ويقلل احتمال الخطأ البشري.
دمج الـ Mocks في اختبارات الوحدة
دمج الـ mocks في اختبارات الوحدة عملية مباشرة، خاصة عند استخدام قالب AutoMockable في Sourcery. فباتباع بضع خطوات بسيطة، يستطيع المطورون إنشاء كائنات mock واستخدامها في اختباراتهم بسهولة، لضمان أن يتصرف الكود كما هو متوقع. ويشمل ذلك إعداد كائن الـ mock، وضبط سلوكه، والتحقق من استدعاء الـ methods الصحيحة. فمثلًا، بعد توليد الـ mock class، يمكنكم تعيينه في خاصية الـ delegate واستخدامه للتحقق من استدعاء methods معينة بالوسائط المتوقعة. ولا يقتصر هذا النهج على التحقق من الوظائف، بل يضمن أيضًا معالجة التفاعلات بين المكونات بشكل صحيح.
أفضل الممارسات والمصادر
عند العمل مع كائنات الـ mock وSourcery، هناك عدة ممارسات جيدة يجب مراعاتها، منها:
- إبقاء كائنات الـ mock بسيطة ومركزة على سلوكيات محددة: تجنبوا تعقيد الـ mocks أكثر من اللازم، فيجب أن تحاكي فقط السلوك الضروري للاختبار.
- استخدام قالب AutoMockable في Sourcery لتوليد mock classes تلقائيًا: استفيدوا من قدرات Sourcery في أتمتة إنشاء الـ mocks لضمان الاتساق وتوفير الوقت.
- اختبار كائنات الـ mock جيدًا للتأكد من أنها تتصرف كما هو متوقع: حتى لو كانت الـ mocks مولّدة تلقائيًا، من الضروري التحقق من أنها تعمل بشكل صحيح داخل اختباراتكم.
- الاستعانة بمصادر مثل توثيق Sourcery ومنتديات المجتمع لمواكبة أحدث الميزات وأفضل الممارسات: تفاعلوا مع المجتمع واستفيدوا من المصادر المتاحة لتعميق فهمكم لـ Sourcery وتحسين استخدامكم له.
من المصادر الموصى بها لمعرفة المزيد عن كائنات الـ mock وSourcery:
- توثيق Sourcery ومستودعه على GitHub
- منتديات مجتمع Swift وموقع Stack Overflow
- الدروس والأدلة حول استخدام Sourcery وكائنات الـ mock في تطوير Swift
الخلاصة
ختامًا، كائنات الـ mock أداة قوية لاختبارات الوحدة في تطوير Swift، وقالب AutoMockable في Sourcery يسهّل توليد mock classes تلقائيًا. وبفهم طريقة استخدام كائنات الـ mock ودمجها في اختبارات الوحدة، يضمن المطورون أن يتصرف الكود كما هو متوقع وأن يُختبر بشكل شامل. وباتباع أفضل الممارسات ومواكبة أحدث المصادر والميزات، يستطيع المطورون الارتقاء باختبارات الوحدة إلى مستوى جديد وتقديم برمجيات عالية الجودة. وتبنّي هذه الأدوات والأساليب لا يرفع الإنتاجية فحسب، بل يساهم أيضًا في بناء تطبيقات متينة وسهلة الصيانة.


