اختبارات C بدون رسائل نصية وتسجيل

قطة لقطة في الآونة الأخيرة ، كتب zerocost مقالًا مثيرًا للاهتمام ، "الاختبارات في C ++ بدون وحدات ماكرو وذاكرة ديناميكية" ، والتي تناقش إطار عمل بسيط لاختبار رمز C ++. تمكن المؤلف (تقريبًا) من تجنب استخدام وحدات الماكرو لتسجيل الاختبارات ، ولكن بدلاً من ذلك ، ظهرت قوالب "سحرية" في الكود ، والتي تبدو لي شخصيًا ، آسف ، بشعة لا يمكن تصورها. بعد قراءة المقال ، كان لدي شعور غامض بعدم الرضا ، لأنني كنت أعرف ما الذي يمكن عمله بشكل أفضل. لم أكن أتذكر على الفور أين ، لكنني بالتأكيد شاهدت رمز الاختبار ، الذي لا يحتوي على حرف إضافي واحد لتسجيله:


void test_object_addition() { ensure_equals("2 + 2 = ?", 2 + 2, 4); } 

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


(KDPV مأخوذة من موقع Cutter تحت CC BY-SA.)


ما هي الحيلة؟


يتم تجميع رمز الاختبار في مكتبة مشتركة منفصلة. يتم استخراج وظائف الاختبار من رموز المكتبة المصدرة وتحديد الأسماء. يتم إجراء الاختبارات بواسطة أداة مساعدة خارجية خاصة. جلس سابينتي.


 $ cat test_addition.c #include <cutter.h> void test_addition() { cut_assert_equal_int(2 + 2, 5); } 

 $ cc -shared -o test_addition.so \ -I/usr/include/cutter -lcutter \ test_addition.c 

 $ cutter . F ========================================================================= Failure: test_addition <2 + 2 == 5> expected: <4> actual: <5> test_addition.c:5: void test_addition(): cut_assert_equal_int(2 + 2, 5, ) ========================================================================= Finished in 0.000943 seconds (total: 0.000615 seconds) 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s) 0% passed 

فيما يلي مثال من وثائق Cutter . يمكنك التنقل بأمان عبر كل ما يتعلق بـ Autotools ، وإلقاء نظرة فقط على الكود. الإطار غريب بعض الشيء ، نعم ، مثل كل شيء ياباني.


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


التفاصيل وخيارات التنفيذ


فكر في بعض المهام التي تحتاج إلى حلها عند كتابة إطار عمل للاختبار باستخدام نهج Cutter.


الحصول على وظائف تصديرها


أولاً ، تحتاج إلى الوصول إلى وظائف الاختبار بطريقة أو بأخرى. بالطبع ، لا يصف معيار C ++ المكتبات المشتركة على الإطلاق. حصل Windows مؤخرًا على نظام فرعي لنظام Linux ، والذي يسمح بتخفيض أنظمة التشغيل الرئيسية الثلاثة إلى POSIX. كما تعلم ، توفر أنظمة POSIX الوظائف dlopen() ، dlsym() ، dlclose() ، والتي يمكنك من خلالها الحصول على عنوان الوظيفة ، ومعرفة اسم رمزها ، و ... هذا كل شيء. لم يتم الكشف عن قائمة الوظائف الموجودة في المكتبة المحملة بواسطة POSIX.


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


كتقدير أولي ، يمكنك ببساطة استدعاء الأداة المساعدة nm :


 $ cat test.cpp void test_object_addition() { } 

 $ clang -shared test.cpp 

 $ nm -gj ./a.out __Z20test_object_additionv dyld_stub_binder 

تحليل ناتجها واستخدام dlsym() .


للحصول على تأمل أعمق ، تعد المكتبات مثل libelf و libMachO و pe- parse مفيدة ، مما يتيح لك تحليل الملفات القابلة للتنفيذ والمكتبات ذات الأنظمة الأساسية التي تهمك برمجياً. في الواقع ، نانومتر والشركة مجرد استخدامها.


اختبار وظيفة الترشيح


كما لاحظت ، تحتوي المكتبات على بعض الأحرف الغريبة:


 __Z20test_object_additionv dyld_stub_binder 

هذا ما هو __Z20test_object_additionv ، عندما أطلقنا على الدالة مجرد test_object_addition ؟ وما هذا اليسار dyld_stub_binder ؟


الأحرف " __Z20... " __Z20... هي زخرفة الاسم __Z20... (اسم الاسم). ميزة تجميع C ++ ، لا شيء يمكن القيام به ، العيش معها. هذا هو ما تسمى الوظائف من وجهة نظر النظام (و dlsym() ). لإظهارها لشخص في شكله العادي ، يمكنك استخدام مكتبات مثل libdemangle . بالطبع ، تعتمد المكتبة التي تحتاجها على المترجم الذي تستخدمه ، لكن تنسيق الزخرفة يكون عادة هو نفسه داخل إطار المنصة.


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


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


اجتياز سياق اختبار قابل للتنفيذ


يتم جمع ملفات الكائنات مع الاختبارات في مكتبة مشتركة ، يتم التحكم في تنفيذ التعليمات البرمجية بالكامل بواسطة أداة مساعدة خارجية - cutter لـ Cutter. وفقا لذلك ، وظائف الاختبار الداخلي يمكن استخدام هذا.


على سبيل المثال ، يمكن تمرير سياق الاختبار القابل للتنفيذ ( IRuntime في المقالة الأصلية) بأمان عبر متغير عام (مؤشر ترابط محلي). السائق هو المسؤول عن إدارة وتمرير السياق.


في هذه الحالة ، لا تتطلب وظائف الاختبار وسيطات ، ولكنها تحتفظ بجميع الميزات المتقدمة ، مثل التسمية التعسفية للحالات التي تم اختبارها:


 void test_vector_add_element() { testing::description("vector size grows after push_back()"); } 

تصل وظيفة description() إلى IRuntime خلال متغير IRuntime ، وبالتالي يمكنها تمرير تعليق إلى إطار عمل لشخص ما. يتم ضمان أمان استخدام السياق العالمي من خلال الإطار وليس مسؤولية كاتب الاختبار.


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


البنائين والمدمرات


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


تستخدم مكتبة Cutter الوظائف التالية لهذا:


  • cut_setup() - قبل كل اختبار فردي
  • cut_teardown() - بعد كل اختبار فردي
  • cut_startup() - قبل تشغيل جميع الاختبارات
  • cut_shutdown() - بعد الانتهاء من جميع الاختبارات

يتم استدعاء هذه الوظائف فقط إذا تم تحديدها في ملف الاختبار. يمكنك وضع إعداد بيئة الاختبار الخاصة بها وتنظيفها (أداة التثبيت): إنشاء الملفات المؤقتة الضرورية ، والإعداد الصعب للكائنات التي تم اختبارها ، ومضادات الاختبار الأخرى.


بالنسبة لـ C ++ ، من الممكن التوصل إلى واجهة أكثر تعبيرًا:


  • أكثر وجوه المنحى ونوع آمن
  • مع دعم أفضل مفهوم RAII
  • باستخدام lambdas لتنفيذ المؤجلة
  • تنطوي على اختبار سياق التنفيذ

لكن الآن أنا أفكر في هذا مرة أخرى بالتفصيل الآن.


الملفات التنفيذية اختبار مستقل


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


أخرى


يحتوي Cutter والأطر الأخرى أيضًا على العديد من الأشياء المفيدة التي يمكن أن تجعل الحياة أسهل عند كتابة الاختبارات:


  • بيانات اختبار مرنة وقابلة للتمديد
  • بناء والحصول على بيانات الاختبار من الملفات
  • دراسات تتبع المكدس ، والاستثناء والتعامل مع انخفاض
  • "مستويات انهيار" قابلة للتخصيص من الاختبارات
  • تشغيل الاختبارات في عمليات متعددة

يجدر النظر إلى الأطر الحالية عند كتابة دراجتك. UX هو موضوع أعمق بكثير.


الخاتمة


تتيح الطريقة المستخدمة في إطار عمل Cutter تحديد وظائف الاختبار مع الحد الأدنى من الحمل المعرفي على المبرمج: ما عليك سوى كتابة وظائف الاختبار وهذا كل شيء. لا يتطلب الرمز استخدام أي قوالب خاصة أو وحدات ماكرو ، مما يزيد من سهولة قراءتها.


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


تشمل عيوب هذا النهج صعوبة إجراء الاختبارات في نفس الملف (نفس وحدة الترجمة) مثل الكود الرئيسي. لسوء الحظ ، في هذه الحالة ، بدون تلميحات إضافية ، لم يعد من الممكن تحديد الوظائف التي يجب تشغيلها وأيها لا. لحسن الحظ ، من المعتاد في C ++ توزيع الاختبارات والتنفيذ في ملفات مختلفة.


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


 void test_object_addition() { ensure_equals(2 + 2, 5); } 

ولكن في نفس الوقت الاحتفاظ بنفس محتوى المعلومات الخاص بالمشكلة في حالة وجود أخطاء:


 Failure: test_object_addition <ensure_equals(2 + 2, 5)> expected: <5> actual: <4> test.c:5: test_object_addition() 

يمكن استخراج اسم الوظيفة التي يتم اختبارها واسم الملف ورقم السطر الخاص ببدء الوظيفة من الناحية النظرية من معلومات التصحيح الموجودة في المكتبة التي يتم جمعها. القيمة المتوقعة والفعلية للتعبيرات المقارنة معروفة ensure_equals() . يسمح لك الماكرو "باستعادة" الإملاء الأصلي لبيان الاختبار ، والذي من الواضح أنه أكثر من السبب وراء توقع القيمة 4 .


ومع ذلك ، هذا ليس للجميع. هل تنتهي فائدة وحدات الماكرو لرمز الاختبار هناك؟ لم أفكر حقًا في هذه اللحظة حتى الآن ، والتي قد تتحول إلى حقل جيد لمزيد من التفاصيل الانحرافات البحث. سؤال أكثر إثارة للاهتمام: هل من الممكن بطريقة ما إنشاء إطار وهمي لـ C ++ بدون وحدات ماكرو؟


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

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


All Articles