اختبار التكامل للتحقق من تسرب الذاكرة

نكتب اختبارات وحدة كثيرة ، وتطوير تطبيق SoundCloud لنظام التشغيل iOS. اختبارات وحدة تبدو رائعة جدا. إنها قصيرة ، (نأمل) ، وتمنحنا الثقة بأن الشفرة التي نكتبها تعمل كما هو متوقع. لكن اختبارات الوحدة ، كما يوحي اسمها ، تغطي مجموعة واحدة فقط من التعليمات البرمجية ، وغالبًا ما تكون وظيفة أو فئة. لذا ، كيف يمكنك اكتشاف الأخطاء الموجودة في التفاعلات بين الفئات - أخطاء مثل تسرب الذاكرة ؟


تسرب الذاكرة


في بعض الأحيان يكون من الصعب للغاية اكتشاف خطأ تسرب للذاكرة. هناك إمكانية لإشارة قوية إلى المفوض ، ولكن هناك أيضًا أخطاء يصعب اكتشافها. على سبيل المثال ، هل من الواضح أن الكود التالي قد يحتوي على تسرب للذاكرة؟

final class UseCase { weak var delegate: UseCaseDelegate? private let service: Service init(service: Service) { self.service = service } func run() { service.makeRequest(handleResponse) } private func handleResponse(response: ServiceResponse) { // some business logic and then... delegate.operationDidComplete() } } 


نظرًا لأن الخدمة قيد التنفيذ بالفعل ، فلا توجد ضمانات فيما يتعلق بسلوكها. تمرير وظيفة handleResponse إلى وظيفة خاصة ، والتي تجسد الذات ، نحن نوفر للخدمة إشارة قوية إلى UseCase . إذا قررت الخدمة الإبقاء على هذا الرابط - وليس لدينا ما يضمن عدم حدوث ذلك - فسيحدث تسرب للذاكرة. لكن من خلال دراسة خاطفة للرمز ، ليس من الواضح أن هذا يمكن أن يحدث بالفعل.

هناك أيضًا منشور رائع لجون ساندل حول استخدام اختبارات الوحدة للكشف عن تسرب الذاكرة للصفوف. ولكن مع المثال أعلاه ، حيث من السهل جدًا تخطي تسرب للذاكرة ، فليس من الواضح دائمًا كيفية كتابة اختبار الوحدة هذا. (بالطبع ، نحن لا نتحدث هنا من حيث الخبرة.)

كما كتب Guilherme في منشور حديث ، فإن الميزات الجديدة في تطبيق SoundCloud لنظام iOS تتم كتابتها وفقًا لـ "الأنماط المعمارية النظيفة" - وغالبًا ما يكون هذا النوع من الشخصيات VIPER . تم تصميم معظم وحدات VIPER باستخدام ما نسميه ModuleFactory . يأخذ مثل ModuleFactory بعض المدخلات والتبعيات والتكوين - ويقوم بإنشاء UIViewController المتصل بالفعل ببقية الوحدة ويمكن دفعه إلى مكدس التنقل.

يمكن أن تحتوي وحدة VIPER هذه على العديد من المندوبين والمراقبين والأخطاء الجامحة ، والتي يمكن أن يؤدي كل منها إلى بقاء وحدة التحكم في الذاكرة بعد إزالتها من حزمة التنقل. عندما يحدث هذا ، ستزيد مساحة الذاكرة ، وقد يقرر نظام التشغيل إيقاف التطبيق.

فهل من الممكن تغطية العديد من التسريبات المحتملة عن طريق كتابة أقل عدد ممكن من اختبارات الوحدة؟ إذا لم يكن الأمر كذلك ، فكل هذا كان مضيعة هائلة للوقت.

اختبارات التكامل


الإجابة ، كما قد تكون خمنت من عنوان هذا المنشور ، هي نعم. ونحن نفعل هذا من خلال اختبار التكامل. الغرض من اختبار التكامل هو اختبار كيفية تفاعل الكائنات مع بعضها البعض. بالطبع ، وحدات VIPER هي مجموعات من الكائنات ، وتسريبات الذاكرة هي أحد أشكال التفاعل الذي نريد بالتأكيد تجنبه.

خطتنا بسيطة: سنستخدم وحدة ModuleFactory الخاصة بنا لإنشاء نموذج وحدة VIPER . بعد ذلك ، سنقوم بإزالة الرابط الخاص بـ UIViewController والتأكد من إتلاف جميع الأجزاء المهمة في الوحدة.

المشكلة الأولى التي نواجهها هي أنه بطبيعته لا يمكننا الوصول بسهولة إلى أي جزء من وحدة VIPER بخلاف UIViewController . الوظيفة العامة الوحيدة في ModuleFactory لدينا هي func make () -> UIViewController . ولكن ماذا لو أضفنا نقطة دخول أخرى فقط لاختباراتنا؟ سيتم الإعلان عن هذه الطريقة الجديدة عبر داخلي ، لذلك لا يمكننا الوصول إليها إلا من خلال testable importing ، ModuleFactory Framework. سيعود الارتباطات إلى جميع الأجزاء الأكثر أهمية في الوحدة ، والتي يمكن أن نحتفظ بها بعد ذلك لكي تدخل الروابط الضعيفة في اختبارنا. يبدو في النهاية مثل هذا:

 public final class ModuleFactory { //     ,   ... public func make() -> UIViewController { makeAndExpose().view } typealias ModuleComponents = ( view: UIViewController, presenter: Presenter, Interactor: Interactor ) func makeAndExpose() -> ModuleComponents { // Set up code, and then... return ( view: viewController, presenter: presenter, interactor: interactor ) } } 


هذا يحل مشكلة عدم وجود الوصول المباشر إلى بيانات الكائن. من الواضح أن هذا ليس مثاليًا ، لكنه يلبي احتياجاتنا ، لذلك دعنا ننتقل إلى كتابة الاختبار. سيبدو مثل هذا:

 final class ModuleMemoryLeakTests: XCTestCase { //      .     //    . private var view: UIViewController? //        //   ,    // UIKit,   UIViewController  . private weak var presenter: Presenter? private weak var interactor: Interactor? //   setUp    ModuleFactory  //   makeAndExpose.     ,   //     ModuleComponents // ,          . //     . func setUp() { super.setUp() let moduleFactory = ModuleFactory(/* mocked dependencies & config */) let components = moduleFactory.makeAndExpose() view = components.view presenter = components.presenter interactor = components.interactor } //   ,   tearDown   , //        ,     ,   //     . func tearDown() { view = nil presenter = nil interactor = nil super.tearDown() } func test_module_doesNotLeakMemory() { //   ,      . //      ,  //          setUp. XCTAssertNotNil(presenter) XCTAssertNotNil(interactor) //        . //    ,   //     ,    //      . view = nil // ,  ,    //  Presenter  Interactor   . //  ,       //  ,    . XCTAssertNil(presenter) XCTAssertNil(interactor) } } 


لذلك ، لدينا طريقة سهلة للكشف عن تسرب الذاكرة في وحدة VIPER . إنه ليس مثاليًا على الإطلاق ويتطلب عمل مستخدم معين لكل وحدة نمطية جديدة نريد اختبارها ، لكن هذا بالتأكيد عمل أقل بكثير من كتابة اختبارات وحدة منفصلة لكل تسرب ممكن للذاكرة. كما أنه يساعد في تحديد تسرب الذاكرة الذي لا نشك فيه. في الواقع ، بعد كتابة العديد من هذه الاختبارات ، تبين أن لدينا اختبارًا لا ينجح ، وبعد إجراء بعض الأبحاث ، وجدنا تسربًا للذاكرة في الوحدة. بعد التصحيح ، يجب تكرار الاختبار.

كما أنه يوفر لنا نقطة بداية لكتابة مجموعة أكثر عمومية من اختبارات التكامل للوحدات النمطية. في النهاية ، إذا احتفظنا برابط قوي لمقدم العرض واستبدلنا UIViewController بأسلوب وهمي ، فيمكننا أن نخفي إدخال المستخدم ، ثم ندعو طرق مقدم العرض ونتحقق من العرض الوهمي للبيانات في طريقة العرض .

Source: https://habr.com/ru/post/ar459220/


All Articles