اكتب الاختبارات المهمة: تعامل مع الرمز الأكثر تعقيدًا أولاً
نشرت: 2022-03-11هناك الكثير من المناقشات والمقالات والمدونات حول موضوع جودة الكود. يقول الناس - استخدم تقنيات اختبار مدفوعة! الاختبارات أمر "لا بد منه" لبدء أي إعادة بناء ديون! كل هذا رائع ، لكنه عام 2016 وهناك حجم هائل من المنتجات وقواعد الرموز التي لا تزال قيد الإنتاج والتي تم إنشاؤها منذ عشرة أو خمسة عشر أو حتى عشرين عامًا. ليس سراً أن الكثير منهم لديهم رمز قديم بتغطية اختبار منخفضة.
بينما أرغب في أن أكون دائمًا في الطليعة ، أو حتى النازف ، في عالم التكنولوجيا - منخرطًا في مشاريع وتقنيات رائعة جديدة - لسوء الحظ ، هذا ليس ممكنًا دائمًا وغالبًا ما يتعين علي التعامل مع الأنظمة القديمة. أود أن أقول إنه عندما تتطور من الصفر ، فإنك تعمل كمبدع ، وتتقن مادة جديدة. ولكن عندما تعمل على رمز قديم ، فأنت أشبه بجراح - فأنت تعرف كيف يعمل النظام بشكل عام ، لكنك لا تعرف أبدًا على وجه اليقين ما إذا كان المريض سينجو من "عمليتك". ونظرًا لكونه رمزًا قديمًا ، فلا يوجد العديد من الاختبارات المحدثة التي يمكنك الاعتماد عليها. هذا يعني أنه في كثير من الأحيان تكون إحدى الخطوات الأولى هي تغطيتها بالاختبارات. بتعبير أدق ، ليس فقط لتوفير التغطية ، ولكن لتطوير استراتيجية تغطية اختبارية.
في الأساس ، ما كنت بحاجة إلى تحديده هو الأجزاء (الفئات / الحزم) من النظام التي نحتاج إلى تغطيتها بالاختبارات في المقام الأول ، حيث احتجنا إلى اختبارات الوحدة ، حيث ستكون اختبارات التكامل أكثر فائدة وما إلى ذلك. هناك العديد من الطرق المعترف بها للقيام بذلك قد لا يكون هذا النوع من التحليل والتحليل الذي استخدمته هو الأفضل ، ولكنه نوع من المقاربة التلقائية. بمجرد تنفيذ مقاربتي ، يستغرق الأمر أقل وقت ممكن لإجراء التحليل بنفسه بالفعل ، والأهم من ذلك ، أنه يجلب بعض المرح في تحليل الكود القديم.
الفكرة الرئيسية هنا هي تحليل مقياسين - الاقتران (أي اقتران وارد ، أو CA) والتعقيد (أي التعقيد السيكلومي).
الأول يقيس عدد الفصول التي تستخدم صفنا ، لذلك فهو يخبرنا بشكل أساسي عن مدى قرب فصل معين من قلب النظام ؛ كلما زاد عدد الفصول التي تستخدم صفنا ، زادت أهمية تغطيته بالاختبارات.
من ناحية أخرى ، إذا كان الفصل بسيطًا جدًا (على سبيل المثال يحتوي على ثوابت فقط) ، فحتى إذا تم استخدامه بواسطة أجزاء أخرى كثيرة من النظام ، فليس من المهم تقريبًا إنشاء اختبار له. هنا حيث يمكن أن يساعد المقياس الثاني. إذا كان الفصل يحتوي على الكثير من المنطق ، فسيكون التعقيد السيكلوماتي مرتفعًا.
يمكن أيضًا تطبيق نفس المنطق في الاتجاه المعاكس ؛ على سبيل المثال ، حتى إذا لم يتم استخدام فئة من قبل العديد من الفئات وتمثل حالة استخدام واحدة فقط ، فلا يزال من المنطقي تغطيتها باختبارات إذا كان منطقها الداخلي معقدًا.
هناك تحذير واحد: لنفترض أن لدينا فئتين - أحدهما يحتوي على CA 100 والتعقيد 2 والآخر به CA 60 والتعقيد 20. على الرغم من أن مجموع المقاييس أعلى بالنسبة للمقياس الأول ، يجب علينا بالتأكيد تغطيته الثاني أولا. هذا لأن الصف الأول يتم استخدامه من قبل الكثير من الفئات الأخرى ، ولكنه ليس معقدًا للغاية. من ناحية أخرى ، يتم استخدام الفئة الثانية أيضًا بواسطة الكثير من الفئات الأخرى ولكنها أكثر تعقيدًا نسبيًا من الفئة الأولى.
للتلخيص: نحتاج إلى تحديد الفئات ذات التعقيد العالي لـ CA و Cyclomatic. من الناحية الرياضية ، هناك حاجة إلى وظيفة اللياقة التي يمكن استخدامها كتصنيف - f (CA ، التعقيد) - التي تزيد قيمها جنبًا إلى جنب مع CA والتعقيد.
ثبت أن العثور على أدوات لحساب CA والتعقيد لقاعدة التعليمات البرمجية بأكملها ، وتوفير طريقة بسيطة لاستخراج هذه المعلومات بتنسيق CSV ، يمثل تحديًا. خلال بحثي ، عثرت على أداتين مجانيتين ، لذا سيكون من الظلم عدم ذكرهما:
- مقاييس الاقتران: www.spinellis.gr/sw/ckjm/
- التعقيد: cyvis.sourceforge.net/
قليلا من الرياضيات
تكمن المشكلة الرئيسية هنا في أن لدينا معيارين - التعقيد CA و Cyclomatic - لذلك نحتاج إلى دمجهما وتحويلهما إلى قيمة عددية واحدة. إذا كانت لدينا مهمة مختلفة قليلاً - على سبيل المثال ، للعثور على فصل به أسوأ مجموعة من معاييرنا - فسنواجه مشكلة تحسين كلاسيكية متعددة الأهداف:
سنحتاج إلى إيجاد نقطة على ما يسمى جبهة باريتو (الأحمر في الصورة أعلاه). الأمر المثير للاهتمام في مجموعة Pareto هو أن كل نقطة في المجموعة هي حل لمهمة التحسين. عندما نتحرك على طول الخط الأحمر ، نحتاج إلى تقديم حل وسط بين معاييرنا - إذا تحسن أحدهما ، فإن الآخر يزداد سوءًا. هذا يسمى Scalarization والنتيجة النهائية تعتمد على كيفية قيامنا بذلك.
هناك الكثير من التقنيات التي يمكننا استخدامها هنا. لكل منها مزاياها وعيوبها. ومع ذلك ، فإن أكثرها شيوعًا هي التحجيم الخطي والأخرى التي تستند إلى نقطة مرجعية. الخطية هي الأسهل. ستبدو وظيفة اللياقة لدينا كمزيج خطي من CA والتعقيد:
f (CA ، التعقيد) = A × CA + B × التعقيد
حيث A و B بعض المعاملات.
النقطة التي تمثل حلاً لمشكلة التحسين لدينا سوف تكمن على الخط (الأزرق في الصورة أدناه). بتعبير أدق ، سيكون عند تقاطع الخط الأزرق مع خط باريتو الأحمر. مشكلتنا الأصلية ليست بالضبط مشكلة تحسين. بدلا من ذلك ، نحن بحاجة إلى إنشاء وظيفة الترتيب. لنأخذ في الاعتبار قيمتين لوظيفة الترتيب لدينا ، وهما أساسًا قيمتان في عمود الترتيب لدينا:
R1 = A ∗ CA + B ∗ التعقيد و R2 = A CA + B ∗ التعقيد
كلتا الصيغتين المكتوبتين أعلاه عبارة عن معادلات خطوط ، علاوة على أن هذه الخطوط متوازية. مع أخذ المزيد من قيم الترتيب في الاعتبار ، سنحصل على المزيد من الخطوط وبالتالي المزيد من النقاط حيث يتقاطع خط باريتو مع الخطوط الزرقاء (المنقطة). ستكون هذه النقاط عبارة عن فئات مقابلة لقيمة مرتبة معينة.

لسوء الحظ ، هناك مشكلة في هذا النهج. بالنسبة لأي سطر (قيمة الترتيب) ، سيكون لدينا نقاط مع CA صغير جدًا وتعقيد كبير جدًا (والعكس صحيح) ملقاة عليه. يضع هذا على الفور النقاط ذات الاختلاف الكبير بين القيم المترية في أعلى القائمة وهو بالضبط ما أردنا تجنبه.
الطريقة الأخرى للقيام بالتحجيم تعتمد على النقطة المرجعية. النقطة المرجعية هي نقطة ذات قيم قصوى لكلا المعيارين:
(الحد الأقصى (CA) ، الحد الأقصى (التعقيد))
ستكون وظيفة الملاءمة هي المسافة بين النقطة المرجعية ونقاط البيانات:
و (CA ، التعقيد) = √ ((CA ، CA) 2 + (التعقيد − التعقيد) 2 )
يمكننا التفكير في وظيفة اللياقة هذه كدائرة مركزها عند النقطة المرجعية. نصف القطر في هذه الحالة هو قيمة الرتبة. سيكون حل مشكلة التحسين هو النقطة التي تلامس فيها الدائرة جبهة باريتو. سيكون حل المشكلة الأصلية عبارة عن مجموعات من النقاط المقابلة لأنصاف أقطار الدائرة المختلفة كما هو موضح في الصورة التالية (تظهر أجزاء من الدوائر لرتب مختلفة كمنحنيات زرقاء منقطة):
يتعامل هذا النهج بشكل أفضل مع القيم المتطرفة ولكن لا تزال هناك مشكلتان: أولاً - أرغب في الحصول على المزيد من النقاط بالقرب من النقاط المرجعية للتغلب بشكل أفضل على المشكلة التي واجهناها مع الدمج الخطي. ثانيًا - يختلف التعقيد CA و Cyclomatic بطبيعته ولهما مجموعة قيم مختلفة ، لذلك نحتاج إلى تطبيعهما (على سبيل المثال ، بحيث تكون جميع قيم كلا المقياسين من 1 إلى 100).
إليك خدعة صغيرة يمكننا تطبيقها لحل المشكلة الأولى - بدلاً من النظر إلى CA والتعقيد السيكلومي ، يمكننا النظر إلى قيمهما المعكوسة. النقطة المرجعية في هذه الحالة ستكون (0،0). لحل المشكلة الثانية ، يمكننا فقط تسوية المقاييس باستخدام الحد الأدنى من القيمة. هنا هو كيف يبدو:
التعقيد المقلوب والمطبيع - التعقيد المعياري:
(1 + دقيقة (التعقيد)) / (1 + التعقيد) ∗ 100
CA المقلوب والمطبيع - NormCA:
(1 + دقيقة (CA)) / (1 + CA) 100
ملاحظة: لقد أضفت 1 للتأكد من عدم وجود قسمة على 0.
تُظهر الصورة التالية مخططًا يحتوي على القيم المقلوبة:
الترتيب النهائي
نقترب الآن من الخطوة الأخيرة - حساب الرتبة. كما ذكرنا ، أنا أستخدم طريقة النقطة المرجعية ، لذا فإن الشيء الوحيد الذي يتعين علينا القيام به هو حساب طول المتجه وتطبيعه وجعله يتناسب مع أهمية إنشاء اختبار الوحدة لفئة. ها هي الصيغة النهائية:
الرتبة (NormComplexity ، NormCA) = 100 - (NormComplexity 2 + NormCA 2 ) / √2
المزيد من الإحصائيات
هناك فكرة أخرى أود إضافتها ، لكن دعونا أولاً نلقي نظرة على بعض الإحصائيات. فيما يلي رسم بياني لمقاييس اقتران:
المثير في هذه الصورة هو عدد الفصول ذات CA منخفض (0-2). لا يتم استخدام الفئات مع CA 0 على الإطلاق أو أنها خدمات ذات مستوى عالٍ. هذه تمثل نقاط نهاية API ، لذلك من الجيد أن لدينا الكثير منها. لكن الفئات التي تحتوي على CA 1 هي تلك المستخدمة مباشرةً بواسطة نقاط النهاية ولدينا عدد من هذه الفئات أكثر من نقاط النهاية. ماذا يعني هذا من منظور العمارة / التصميم؟
بشكل عام ، هذا يعني أن لدينا نوعًا من النهج الموجه نحو النص - فنحن نكتب كل حالة عمل على حدة (لا يمكننا إعادة استخدام الكود لأن حالات العمل متنوعة للغاية). إذا كان هذا هو الحال ، فمن المؤكد أنها رائحة كود ونحتاج إلى إعادة البناء. بخلاف ذلك ، فهذا يعني أن تماسك نظامنا منخفض ، وفي هذه الحالة نحتاج أيضًا إلى إعادة البناء ، ولكن إعادة البناء المعماري هذه المرة.
المعلومات المفيدة الإضافية التي يمكننا الحصول عليها من المدرج التكراري أعلاه هي أنه يمكننا تصفية الفئات ذات الاقتران المنخفض تمامًا (CA في {0،1}) من قائمة الفئات المؤهلة للتغطية باختبارات الوحدة. ومع ذلك ، تعتبر نفس الفصول مرشحة جيدة لاختبارات التكامل / الاختبارات الوظيفية.
يمكنك العثور على جميع البرامج النصية والموارد التي استخدمتها في مستودع GitHub هذا: ashalitkin / code-base-stats.
هل تعمل دائما؟
ليس بالضرورة. بادئ ذي بدء ، الأمر كله يتعلق بالتحليل الثابت ، وليس وقت التشغيل. إذا كان الفصل مرتبطًا بالعديد من الفئات الأخرى ، فقد يكون ذلك علامة على أنه مستخدم بكثافة ، لكنه ليس صحيحًا دائمًا. على سبيل المثال ، لا نعرف ما إذا كانت الوظيفة مستخدمة بشكل كبير من قبل المستخدمين النهائيين. ثانيًا ، إذا كان تصميم وجودة النظام جيدًا بما فيه الكفاية ، فمن المرجح أن تكون الأجزاء / الطبقات المختلفة منه مفصولة عبر واجهات ، لذا فإن التحليل الثابت لـ CA لن يعطينا صورة حقيقية. أعتقد أنه أحد الأسباب الرئيسية لعدم شعبية CA في أدوات مثل Sonar. لحسن الحظ ، هذا جيد تمامًا بالنسبة لنا ، لأنك ، إذا كنت تتذكر ، فنحن مهتمون بتطبيق هذا تحديدًا على قواعد الرموز القديمة القبيحة.
بشكل عام ، أود أن أقول إن تحليل وقت التشغيل من شأنه أن يعطي نتائج أفضل بكثير ، لكنه للأسف أكثر تكلفة ، ويستغرق وقتًا طويلاً ، ومعقدًا ، لذا فإن نهجنا هو بديل يحتمل أن يكون مفيدًا ومنخفض التكلفة.
