عند ترحيل اختبارات الوحدات من XCTest على Mac، حافظ على تسلسل الملكية: أكمِل العمل، وحرّر المراجع القوية المقصودة، ثم تحقّق من المرجع الضعيف. في التنظيف المتزامن، تجعل عبارة defer محلية للاختبار هذا التسلسل واضحًا.
يركّز هذا الشرح خطوة بخطوة على تحويل اختبار وحدة حساس للملكية في Xcode. يحسب المثال عدد الخلايا في شبكة اصطناعية، ثم يتحقق من تحرير الحاسبة. وهو يستخدم كائنات Swift محلية وحسابًا بالأعداد الصحيحة فقط.
ما سلوك دورة الحياة الذي يجب الحفاظ عليه عند الترحيل؟
يوفر XCTest تسلسلًا تديره المنظومة. بعد انتهاء طريقة الاختبار، يشغّل كتل التنظيف المسجلة بترتيب عكسي للتسجيل، ثم يشغّل أساليب تنظيف المثيل. يستمر تشغيل التنظيف المسجل حتى عند إخفاق تأكيد في الاختبار؛ أما إنهاء العملية فقد يمنع ذلك. تهم هذه التفاصيل عندما تحرر إحدى الكتل كائنًا وتتحقق كتلة أخرى من مرجعه الضعيف. إعداد الحالة وتنظيفها في اختباراتك
على سبيل المثال، يجب أن تُنفّذ كتلة تنظيف تتحقق من nil بعد الكتلة التي تزيل المالك المقصود الأخير. وبما أن XCTest يستخدم ترتيب التسجيل العكسي، يُسجّل التحقق أولًا والتحرير ثانيًا. وعكس ترتيب التسجيل يغيّر ما يرصده الاختبار.
يربط دليل الترحيل إلى Swift Testing الإعداد بمهيئ والتنظيف بمزيل تهيئة في مجموعة من الفئة أو الممثل. وهذا الربط مفيد للتنظيف المرتبط بعمر المجموعة. لكن تأكيد الملكية يتطلب قرارًا إضافيًا: ما المرجع الذي ينبغي أن يختفي تحديدًا قبل إجراء التأكيد؟ ترحيل اختبار من XCTest
استخدم جدول القرارات هذا قبل تغيير صياغة الاختبار:
| المسؤولية الحالية | خيار الترحيل | السبب |
|---|---|---|
| إنشاء حالة تجهيز جديدة لكل اختبار مثيل | تهيئة خاصية مخزنة أو init() |
يجهّز حالة الاختبار قبل تشغيل الاختبار |
| تحرير كائن والتحقق فورًا من مرجع ضعيف | تحرير صريح يتبعه #expect، عادةً داخل كتلة defer محلية |
يوضح موضع الرصد |
| تنفيذ تنظيف متزامن مرتبط بعمر مجموعة مرجعية النوع | مجموعة من فئة أو ممثل مع deinit |
يتبع ربط دورة الحياة الموثق |
| انتظار التنظيف أو الإبلاغ عن خطأ فيه | تنظيف منتظَر داخل دالة غير متزامنة، أو نطاق مخصص مناسب | يتطلب مسار تنفيذ يدعم التعليق ومعالجة الأخطاء |
الجدول أداة مساعدة لاتخاذ قرارات الترحيل، مستمدة من قواعد دورة الحياة واللغة الموثقة. ولا يعني أن لكل خطاف في XCTest طريقة مكافئة في Swift Testing.
لماذا قد يؤدي نقل التحقق من المرجع الضعيف إلى deinit إلى تغيير الاختبار؟
تستدعي Swift مزيل تهيئة الفئة مباشرة قبل تحرير المثيل. ويظل بإمكان مزيل التهيئة الوصول إلى الخصائص المخزنة لذلك المثيل. لذلك، فإن دخول deinit الخاص بالمجموعة لا يثبت بحد ذاته أن كائنًا تحتفظ به إحدى تلك الخصائص قد حُرر بالفعل. إزالة التهيئة
تخيّل مجموعة لها خاصية قوية layout. يمكن أن يظل المرجع الضعيف إلى ذلك التخطيط غير-nil أثناء تنفيذ مزيل تهيئة المجموعة. يجب أن يزيل الاختبار مرجع الخاصية القوي قبل التحقق مما إذا كان التخطيط قد حُرر.
هناك حالة إخفاق أخرى: قد تمنع دورة مراجع تحتفظ بالمجموعة استدعاء مزيل تهيئتها. وعندئذ لن تتاح لأي تأكيد موضوع داخل مزيل التهيئة وحده فرصة الإبلاغ عن الاحتفاظ. وينبع ذلك من قواعد الملكية في ARC: فالمراجع القوية تُبقي المثيلات حية، بينما لا تمنع المراجع الضعيفة تحرير الذاكرة وتصبح nil عند تحرير المثيل المشار إليه. العد التلقائي للمراجع
في اختبار الملكية، أبقِ المراقب خارج الكائن الذي يجري اختبار تحريره. أتح للاختبار عملية تحرير محددة، ونفّذ التأكيد ضمن مسار التنفيذ المعتاد للاختبار. يجعل هذا الترتيب عقد الاختبار قابلًا للفحص من دون الاعتماد على تحرير المجموعة نفسها من الذاكرة.
أنشئ خط أساس XCTest في Xcode
استخدم هدف اختبارات وحدات موجودًا في Xcode 16 أو أحدث. توثّق Apple أن XCTest وSwift Testing يمكن أن يتواجدا معًا في الهدف والحزمة نفسيهما، لذا لا يتطلب الترحيل حزمة أخرى لاختبارات الوحدات. لإضافة ملف اختبار، استخدم File > New > New File From Template واختر XCTest Unit Test أو Swift Testing Unit Test بحسب الحاجة. إضافة اختبارات إلى مشروع Xcode
فيما يلي ملف اختبار XCTest كامل. تُرجع الحاسبة فيه 3 * 4، ما يعطي نتيجة متوقعة قدرها 12. يكشف تسجيلا التنظيف عمدًا ترتيب التنفيذ المطلوب.
import XCTest
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
final class GridCalculatorXCTests: XCTestCase {
private var calculator: GridCalculator?
override func setUp() {
super.setUp()
calculator = GridCalculator()
}
func testCellCountAndRelease() {
weak var observed = calculator
// Registered first, so this executes second.
addTeardownBlock { [weak observed] in
XCTAssertNil(observed)
}
// Registered second, so this executes first.
addTeardownBlock { [self] in
calculator = nil
}
XCTAssertEqual(
calculator?.cellCount(columns: 3, rows: 4),
12
)
}
}
تحتفظ كتلة التحرير بحالة الاختبار كي تتمكن من تحديث خاصية التجهيز. وتلتقط كتلة الرصد الحاسبة بمرجع ضعيف، لتجنب إضافة مالك قوي آخر في التحقق نفسه. توثّق Apple أن كتل التنظيف هي المكان المناسب للتنظيف الخاص بالاختبار، والمسجل إلى جانب الإعداد المرتبط به. إعداد الحالة وتنظيفها في اختباراتك
عقد خط الأساس محدد بدقة: بعد انتهاء جسم الاختبار ومسح خاصية التجهيز، يجب ألا تعود الحاسبة موجودة. ولا يقيس ذلك استهلاك الذاكرة، ولا يثبت تحرير كل كائن أنشأه التطبيق.
رحّل تسلسل الملكية إلى نطاق محلي
في هذه الحالة المتزامنة، استخدم مجموعة بنوع struct وضع التحرير والتحقق في كتلة defer واحدة. تنفّذ Swift التعليمات المؤجلة عند مغادرة النطاق المحيط بها، بما في ذلك عند إرجاع مبكر أو رمي خطأ. وتُنفّذ كتل التأجيل المتعددة بترتيب عكسي؛ أما الكتلة الواحدة فتجعل تسلسل التحرير قبل التحقق في هذا المثال أسهل قراءة. تدفق التحكم
فيما يلي ملف Swift Testing كامل ومستقل. استبدل به ملف خط الأساس، أو أبقِ المثالين في ملفين منفصلين بحيث تظل الأنواع المساعدة الخاصة بهما منفصلة. الاختبار الثاني حالة تحكم تشخيصية: يوضح أن مالكًا قويًا إضافيًا يُبقي المرجع الضعيف مشيرًا إلى الكائن حتى يُزال ذلك المالك.
import Testing
private final class GridCalculator {
func cellCount(columns: Int, rows: Int) -> Int {
columns * rows
}
}
private final class GridFixture {
var calculator: GridCalculator? = GridCalculator()
var additionalOwner: GridCalculator?
}
struct GridCalculatorTests {
@Test
func cellCountAndRelease() throws {
let fixture = GridFixture()
weak var observed = fixture.calculator
defer {
fixture.calculator = nil
#expect(observed == nil)
}
let result = try #require(
fixture.calculator?.cellCount(columns: 3, rows: 4)
)
#expect(result == 12)
}
@Test
func weakProbeDetectsAnAdditionalOwner() {
let fixture = GridFixture()
weak var observed = fixture.calculator
fixture.additionalOwner = fixture.calculator
fixture.calculator = nil
#expect(observed != nil)
fixture.additionalOwner = nil
#expect(observed == nil)
}
}
في الاختبار الأول، تملك حالة التجهيز الحاسبة، ويراقبها observed من دون امتلاكها. ينتج عن العملية الحسابية عدد صحيح اختياري، وتزيل #require اختياريته. ولا يُخزّن أي كائن منزوع الاختيارية في متغير محلي آخر. عند الخروج من النطاق، تمسح الكتلة المؤجلة مرجع حالة التجهيز وتتحقق من المراقب.
يسجل #expect الفاشل مشكلة ويسمح باستمرار التنفيذ. أما #require الفاشل فيرمي خطأً، فيوقف مسار التنفيذ هذا. وبما أن defer مسجل قبل المتطلب، يستمر تنفيذ التنظيف والتحقق من المرجع الضعيف حتى عندما يرمي المتطلب خطأً. التوقعات والتأكيدات، معالجة الأخطاء
تثبت حالة التحكم التشخيصية وجود حالتين متوقعتين منفصلتين: مسح أحد المالكين من أصل اثنين يُبقي المراقب غير-nil؛ ومسح المالك المتبقي يجعله nil. هذه فحوص توضيحية للملكية، وليست نتائج تطبيق مقاسة. جرى فحص أمثلة Swift، لكن تعذر تجميعها في بيئة البحث لعدم توفر برنامج Swift التنفيذي.
تحقّق من حدود المساعدات والتزامن والتنظيف غير المتزامن
ينتمي defer محلي إلى النطاق المحيط به. ونقل الكتلة المؤجلة إلى دالة مساعدة مُنشئة يجعلها تُنفّذ عند عودة تلك الدالة، وقد يحدث ذلك قبل استخدام الاختبار للكائن المُعاد. أبقِ الكتلة داخل الاختبار، أو صمّم دالة مساعدة تنفّذ العملية بأكملها داخل نطاقها. فالنطاق المحيط جزء من عقد الملكية. تدفق التحكم
تحقّق أيضًا من موضع حالة التجهيز. تنشئ Swift Testing مثيل مجموعة مستقلًا لكل طريقة اختبار مثيل، وتُشغّل الاختبارات ضمن المجموعة بالتوازي افتراضيًا. تناسب هذه الآلية الحالة المحلية للمثيل أو للاختبار؛ أما حالة التجهيز العامة أو المجموعة الثابتة فتُدخل ملكية مشتركة بين الاختبارات. تنظيم دوال الاختبار باستخدام أنواع المجموعات
بالنسبة إلى شيفرة تطبيق Mac التي تتطلب الممثل الرئيسي، اعزل اختبار Swift Testing صراحةً باستخدام @MainActor. يشير دليل الترحيل إلى أن Swift Testing لا ترث سلوك الممثل الرئيسي الافتراضي في XCTest لأساليب الاختبار المتزامنة. ولا تتطلب الحاسبة الاصطناعية أعلاه ذلك. ترحيل اختبار من XCTest
الكتلة المؤجلة المتزامنة ليست بديلًا عن التنظيف المنتظَر. لا يمكن لإجراءات التأجيل في Swift رمي خطأ خارج الكتلة. إذا كان التنظيف قد يفشل، فتعامل مع ذلك الإخفاق وأبلغ عنه صراحةً بدلًا من تجاهله. أبقِ التنظيف المنتظَر في مسار تنفيذ غير متزامن، وتأكد من وصول مساري الجسم الناجح والرافع للخطأ إليه. معالجة الأخطاء
لإعداد وتنظيف مُدارين بإطار العمل وقابلين لإعادة الاستخدام، يوفر Swift Testing الحالي TestScoping، الذي يتيح تنفيذًا مخصصًا حول اختبار أو مجموعة. تدرجه Apple ضمن ما يتوفر مع Swift 6.1 وXcode 16.3. وهو مسار مدعوم عندما تحتاج عدة اختبارات إلى نطاق مشترك، لكنه يتطلب سمة مخصصة ومزوّد نطاق بدلًا من مجرد إعادة تسمية addTeardownBlock. TestScoping
شخّص تغيّر نتيجة التحقق من المرجع الضعيف بعد الترحيل
إذا أخفق اختبار الملكية بعد ترحيله، فتحقّق من النقاط التالية بالترتيب:
- تأكد من موضع الرصد. يجب أن يحدث التحرير المقصود قبل التحقق من المرجع الضعيف.
- افحص خصائص التجهيز. قد تظل خاصية متبقية مالكة للحاسبة بعد مسح خاصية أخرى.
- افحص الأسماء المستعارة المحلية وعمليات الالتقاط في الإغلاقات. قد يطيل كائن منزوع الاختيارية أو مرجع ملتقط بقوة مدة الملكية إلى ما بعد التحقق.
- افحص نطاق الدالة المساعدة. قد يُنفّذ التنظيف المؤجل في الدالة المساعدة مبكرًا جدًا بالنسبة إلى عقد الاختبار المقصود.
- افحص العمل الجاري. إذا بدأ الكائن الحقيقي عملًا غير متزامن، فحدّد وقت اكتماله وانتظره قبل تطبيق توقع تحرير متزامن.
يشير المرجع الضعيف غير-nil إلى وجود كائن عند موضع الرصد. ابحث عن المالك المتبقي قبل تغيير التأكيد أو إضافة تأخير. لا يوجد في هذا المثال الاصطناعي عمل غير متزامن ينبغي انتظاره، وتوضح حالة التحكم التشخيصية بدقة أثر المالك الإضافي في النتيجة.
يكتمل الترحيل عندما تعبّر تأكيدات الحساب والملكية عن النتيجة المقصودة نفسها التي عبّر عنها خط الأساس: احسب 12، وأزل الملكية المقصودة لحالة التجهيز، وتحقق من التحرير عبر مرجع ضعيف محتفظ به على نحو مستقل. أبقِ هذا التسلسل واضحًا مع تطور الاختبار.
المصادر
إعداد الحالة وتنظيفها في اختباراتك — توثيق Apple للمطورين.
ترحيل اختبار من XCTest — مشروع Swift.
إزالة التهيئة — مشروع Swift، لغة البرمجة Swift.
العد التلقائي للمراجع — مشروع Swift، لغة البرمجة Swift.
إضافة اختبارات إلى مشروع Xcode — توثيق Apple للمطورين.
تدفق التحكم — مشروع Swift، لغة البرمجة Swift.
التوقعات والتأكيدات — مشروع Swift.
معالجة الأخطاء — مشروع Swift، لغة البرمجة Swift.
تنظيم دوال الاختبار باستخدام أنواع المجموعات — مشروع Swift.
TestScoping — توثيق Apple للمطورين.