كيفية تبسيط اختبارات العمليات الحسابية وجعلها أكثر وضوحاً عبر إعادة هيكلة ذكية

دقائق القراءة: 8

مقدمة: لماذا يجب أن تكون اختبارات الحسابات واضحة ومعبّرة؟

عند كتابة اختبارات الوحدات، من الأفضل أن تكون الاختبارات وصفية بقدر الإمكان، بحيث تشرح سلوك النظام لا أن تتحقق منه فقط. هذا الأسلوب يُعرف بمفهوم Tests as Documentation، أي أن الاختبار نفسه يصبح وثيقة حيّة تشرح ما الذي يجب أن يفعله النظام.

أحد أكثر الأخطاء شيوعاً في هذا النوع من الاختبارات هو استخدام بيانات ثابتة ومشفّرة مباشرة داخل التحقق، وهو ما يُعرف برائحة Hard Coded Test Data. عندما يفشل الاختبار في هذه الحالة، يصبح من الصعب معرفة سبب الفشل أو المنطق المتوقع وراء النتيجة.

على سبيل المثال، الاختبار التالي لا يوضح الكثير:

assert sut.wake_erosion_rate(0.03) == 0.8

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

لكن إذا ضمّنا العملية الحسابية داخل الاختبار، يصبح القصد أوضح بكثير:

ambient_turbulence = 0.03
assert sut.wake_erosion_rate(ambient_turbulence) == 2.5 * ambient_turbulence + 0.05

بهذه الصياغة، يصبح الاختبار أكثر تعبيراً، كما يوضّح للقارئ العلاقة الحسابية المتوقعة مباشرة. ومن الأفضل أيضاً استبدال القيم مثل 2.5 و0.05 بأسماء الثوابت التي تعبّر عن معناها في المجال البرمجي.

اختبارات وحدات للعمليات الحسابية وكيفية تبسيطها عبر إعادة الهيكلة البرمجية

فكرة المقال: كيف نجعل اختبار الحسابات عملياً وقابلاً للصيانة؟

يرتكز هذا الطرح على مبادئ معروفة في كتاب XUnit Test Patterns، وهو من أهم المراجع في أنماط اختبارات الوحدات. الفكرة الأساسية هنا أن بعض الحسابات تكون معقدة لدرجة تجعل تضمينها داخل الاختبار أمراً مرهقاً، خاصة إذا كانت الشيفرة نفسها تحتوي على تفرعات متعددة وحلقات تكرار واعتمادات داخلية متشابكة.

في المثال المطروح، هناك فئة باسم ConstructionMarginCalculator تحتوي على منطق حسابي يقارب 30 سطراً. ورغم أن الحجم ليس ضخماً بحد ذاته، فإن وجود عدة جمل if مع حلقة تكرار ينتج عدداً كبيراً من المسارات المحتملة داخل الشيفرة، يصل إلى 2^9 أي 512 مساراً. ومن الواضح أن اختبار جميع هذه المسارات ليس عملياً ولا مفيداً.

الهدف إذن ليس فقط كتابة مزيد من الاختبارات، بل إعادة هيكلة الشيفرة بحيث تصبح:

  • أسهل للفهم.
  • أقل تفرعاً وتعقيداً.
  • أبسط عند الاختبار.
  • مناسبة لتضمين الحسابات نفسها داخل الاختبارات.

إخراج العملية الحسابية من حلقة التكرار

المشكلة: التكرار يزيد تعقيد الاختبار

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

  • يجب تجهيز قائمة كاملة بدلاً من قيمة واحدة.
  • يجب التحقق من قائمة نتائج بدلاً من نتيجة مفردة.
  • غالباً ستحتاج إلى اختبارات متعددة لحالات القائمة الفارغة، وعنصر واحد، وعدة عناصر.

الحل: اجعل الدالة تحسب عنصراً واحداً فقط

إحدى أبسط وأفضل خطوات إعادة الهيكلة هي نقل منطق التكرار إلى مكان آخر، وجعل الدالة الأساسية مسؤولة عن حساب step واحد فقط. بهذه الطريقة يصبح الاختبار مركّزاً على المنطق الفعلي، لا على تفاصيل المرور على القائمة.

أما الجزء المسؤول عن التكرار، فيمكن اختباره بشكل منفصل، وغالباً باستخدام Mock للتأكد من أن الاستدعاءات تتم كما يجب. وحينها لن تحتاج إلى اختبار التكرار والحساب معاً، بل يكفي اختبار كل مسؤولية على حدة.

الفائدة التقنية

هذا التعديل يقلل الحاجة من ثلاثة اختبارات أساسية إلى اختبار واحد فقط في هذا الجزء، كما يخفّض عدد المسارات المحتملة من 2^9 إلى 2^7، أي من 512 إلى 128 مساراً. ورغم أن الرقم ما يزال كبيراً، فإنه يمثل تقدماً ملحوظاً.

إدخال تجريدات قابلة للمحاكاة

المشكلة: بعض الحسابات الفرعية معقدة بحد ذاتها

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

الحل: حقن مكوّن مستقل مثل InflationCalculator

بدلاً من تضمين منطق التضخم داخل الحاسبة الرئيسية، يمكن تمرير كائن مستقل مثل InflationCalculator. وإذا كانت القيم مثل date_of_financial_close وinflation_rate وinflation_mode ثابتة أثناء دورة الحساب، فيمكن تمريرها إلى باني هذا الكائن.

بذلك، لا تعود الحاسبة الرئيسية بحاجة مباشرة إلى inflation_rate أو inflation_mode، ويصبح من السهل في الاختبارات إنشاء نسخة وهمية mock من InflationCalculator تُرجع قيمة ثابتة، مثل 2.

لماذا هذه الخطوة مهمة؟

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

النتيجة هنا أن عدد المسارات ينخفض من 2^7 إلى 2^3، أي إلى 8 مسارات فقط. صحيح أنك ستحتاج إلى اختبارات منفصلة لـ InflationCalculator نفسه، وربما أربع حالات أساسية، لكن العدد الإجمالي يبقى معقولاً جداً مقارنة بالوضع السابق.

هذا مثال ممتاز على أثر الشيفرة منخفضة الترابط loosely coupled code في تسهيل الاختبار والصيانة.

اختبار الفروع الشرطية بشكل معزول

الفكرة: لا تختبر كل شيء دفعة واحدة

عندما تحتوي الدالة على تفرعات شرطية، ليس من الضروري أن يغطي كل اختبار جميع الفروع في آن واحد. بل من الأفضل غالباً تعطيل بعض الشروط عمداً، حتى تتمكن من اختبار فرع محدد بصورة معزولة وواضحة.

على سبيل المثال، يمكن جعل in_selling_mode تساوي False، وجعل step.start_of_step مختلفاً عن date_of_financial_close. بهذه الطريقة يتم استبعاد أجزاء من المنطق، ويصبح الاختبار معنيّاً فقط بفرع واحد.

ما الذي نكسبه من هذا الأسلوب؟

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

turbine_costs * fraction_of_spend * inflation

هذا النوع من الاختبارات لا يكتفي بالتحقق من صحة النتيجة، بل يشرح أيضاً للقارئ قاعدة العمل المتوقعة. وهنا يتحقق الهدف الحقيقي من Tests as Documentation.

وبسبب تضييق نطاق الاختبار، تظهر فرصة ممتازة لاستخدام Test Data Builder أو دوال مساعدة لإخفاء المعلومات غير المهمة، وتقليل الضجيج داخل الاختبار.

اختبار القيم بشكل معزول

المشكلة: بعض القيم تُعدَّل في أكثر من فرع

أحياناً لا تكفي عزلة الفروع الشرطية وحدها، لأن قيمة معينة قد تُنشأ في البداية ثم تُعدّل لاحقاً داخل فرع آخر. مثال ذلك المتغير balance_of_plant_cost_including_margin، الذي يحصل على قيمة أولية ثم يُحدَّث إذا كان in_selling_mode مفعلاً.

الحل: اختبر قيمة واحدة فقط في كل مرة

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

كما أن استخدام نمط Test Data Builder هنا يساعد على إخفاء Irrelevant Information، أي التفاصيل التي لا تؤثر في هذا السيناريو تحديداً. والنتيجة هي اختبار أقصر وأكثر تعبيراً.

ومن الملاحظات المهمة أن الشيفرة الحسابية داخل الاختبار قد لا تكون نسخة مطابقة تماماً لشيفرة النظام قيد الاختبار SUT. فإذا كان الاختبار يعرف مسبقاً أن in_selling_mode مفعّل، فلن يحتاج إلى شرط إضافي. وهذا يجعل الاختبار أبسط من الشيفرة الأصلية ويقلل خطر الوقوع في رائحة Obscure Test.

اختبار أجزاء من القيم المعقدة بشكل مستقل

متى نحتاج هذا الأسلوب؟

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

لنأخذ مثال construction_profit، والذي يُحسب بالشكل التالي:

step.turbine_cost_including_margin = turbine_costs * inflation * fraction_of_spend;
step.balance_of_plant_cost_including_margin = balance_of_plant_costs_at_financial_close * inflation * fraction_of_spend;
step.construction_profit = -1 * (step.turbine_cost_including_margin + step.balance_of_plant_cost_including_margin) * epc_margin

كيف نبسّط هذا النوع من الاختبارات؟

هناك قاعدة مفيدة هنا:

  • إذا كان العامل ذا تأثير ضربي، فيمكن ضبطه على 1 حتى يصبح محايداً.
  • إذا كان العامل ذا تأثير جمعي، فيمكن ضبطه على 0 حتى يختفي أثره.

بناءً على ذلك:

  • يمكن ضبط inflation وfraction_of_spend وepc_margin على 1.
  • ويمكن ضبط القيم التي تُجمع على 0 إذا أردت عزل الجزء الآخر من الحساب.

بهذا الأسلوب، تستطيع اختبار جزء بعينه من منطق construction_profit دون الاضطرار إلى حمل كل التعقيد المرافق. وهذا يجعل الاختبار أوضح، وأقصر، وأكثر مقاومة لرائحة Obscure Test.

استخدام نمط اللوح الأسود Blackboard Pattern

ما المشكلة التي يعالجها هذا النمط؟

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

فكرة Blackboard Pattern

هذا النمط يقوم على إنشاء وسيط مشترك، أشبه بلوح تُكتب عليه النتائج الوسيطة. تقوم حاسبة أولى بكتابة قيم مثل turbine_cost_including_margin وbalance_of_plant_cost_including_margin على هذا اللوح، ثم تأتي حاسبة أخرى لتقرأ هذه القيم عند حساب construction_profit.

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

متى يكون هذا النمط مناسباً؟

رغم أن Blackboard Pattern ليس خياراً بسيطاً دائماً، فإنه مفيد جداً في الأنظمة التي تحتوي على سلاسل من الحسابات الوسيطة المعتمدة على بعضها. الشرط الأساسي هو استخدامه بحكمة، لأن الإفراط فيه قد يزيد من التعقيد المعماري إذا استُخدم في موضع لا يستحق.

ملخص عملي لأهم أساليب إعادة الهيكلة

الأسلوب الفائدة الأساسية أثره على الاختبارات
إخراج الحساب من الحلقة عزل منطق التكرار عن منطق الحساب اختبارات أقل وأبسط
إدخال تجريدات قابلة للمحاكاة تقليل الترابط وعزل الحسابات الفرعية سهولة استخدام Mock وتقليل المسارات
اختبار الفروع بشكل معزول التركيز على سيناريو واحد في كل اختبار اختبارات أوضح وأكثر توثيقاً
اختبار القيم بشكل مستقل تقليل الإعداد غير الضروري اختبارات مختصرة ومباشرة
اختبار القيم الجزئية عزل جزء من معادلة معقدة تبسيط الاختبار دون فقدان المعنى
استخدام Blackboard Pattern فك الاعتماد بين النتائج الوسيطة اختبار الأجزاء المعقدة بشكل مستقل

أفضل ممارسات لكتابة اختبارات حسابية قابلة للفهم

  • ضمّن المعادلة داخل الاختبار كلما أمكن، بدلاً من مقارنة الناتج بقيمة جامدة فقط.
  • سمِّ الثوابت والمتغيرات بأسماء تعبّر عن المعنى التجاري أو الحسابي.
  • اجعل كل اختبار يشرح سلوكاً واحداً فقط.
  • افصل بين منطق التكرار، ومنطق القرار، ومنطق الحساب.
  • استخدم Mock وTest Data Builder لإخفاء التفاصيل غير المهمة.
  • إذا كان الاختبار أطول من اللازم، فهذه إشارة إلى أن الشيفرة نفسها قد تحتاج إلى إعادة هيكلة.

الخلاصة التقنية

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *