ترتبط العديد من المكتبات الشائعة للاختبار ، مثل Google Test و Catch2 و Boost.Test ، ارتباطًا كبيرًا باستخدام وحدات الماكرو ، لذلك كمثال للاختبارات على هذه المكتبات ، عادة ما تشاهد صورة مثل هذه:
namespace {
وحدات الماكرو في C ++ حذرة ، لماذا تزدهر في المكتبات لإنشاء اختبارات؟
يجب أن توفر مكتبة اختبار الوحدة لمستخدميها طريقة لكتابة الاختبارات حتى يتمكن وقت التشغيل للاختبار من العثور عليها وتنفيذها بطريقة أو بأخرى. عندما تفكر في كيفية القيام بذلك ، يبدو أن استخدام وحدات الماكرو أسهل. يعرّف الماكرو TEST () عادةً وظيفة ما (في حالة اختبار Google ، يقوم الماكرو أيضًا بإنشاء فئة) ويضمن أن عنوان هذه الوظيفة يدخل في بعض الحاوية العامة.
المكتبة المشهورة التي يتم فيها تنفيذ النهج بدون ماكرو واحد هي إطار توت . دعونا نرى مثالها من البرنامج التعليمي:
#include <tut/tut.hpp> namespace tut { struct basic{}; typedef test_group<basic> factory; typedef factory::object object; } namespace { tut::factory tf("basic test"); } namespace tut { template<> template<> void object::test<1>() { ensure_equals("2+2=?", 2+2, 4); } }
إن الفكرة القائلة بأن الأساس هو أمر مثير للاهتمام وتعمل بشكل جيد ، إنها ليست صعبة للغاية. باختصار ، لديك فئة أساسية تنفذ وظيفة قالب تتضمن تحديد معلمات باستخدام عدد صحيح:
template <class Data> class test_object : public Data { template <int n> void test() { called_method_was_a_dummy_test_ = true; } }
الآن عند كتابة هذا الاختبار:
template<> template<> void object::test<1>() { ensure_equals("2+2=?", 2+2, 4); }
يمكنك بالفعل إنشاء تخصص في طريقة اختبار لعدد محدد N = 1 (هذا هو بالضبط ما يمثل template<>template<>
). عن طريق استدعاء test<N>()
يمكن لوقت تشغيل الاختبار أن يفهم ما إذا كان اختبارًا حقيقيًا أم كان called_method_was_a_dummy_test_
يبحث في القيمة called_method_was_a_dummy_test_
بعد تنفيذ الاختبار.
بعد ذلك ، عندما تعلن مجموعة اختبار:
tut::factory tf("basic test");
أولاً ، تقوم بتعداد جميع test<N>
إلى ثابت معين يتم توصيله بالمكتبة ، وثانياً ، تضيف معلومات جانبية عن المجموعة إلى الحاوية العامة (اسم المجموعة وعناوين جميع وظائف الاختبار).
يتم استخدام الاستثناءات كشروط اختبار في tut ، وبالتالي فإن الدالة tut::ensure_equals()
ستلقي ببساطة استثناءً إذا كانت القيمتان التي تم تمريرها إليها غير متساوية ، tut::ensure_equals()
أن بيئة تشغيل الاختبار ستعتبر استثناء وتعتبر الاختبار فشلًا. يعجبني هذا النهج ، يصبح واضحًا على الفور لأي مطور لـ C ++ حيث يمكن استخدام مثل هذه التأكيدات. على سبيل المثال ، إذا كان اختباري قد أنشأ خيطًا مساعدًا ، فليس من المجدي وضع تأكيدات هناك ، فلن يقبض عليه أحد. بالإضافة إلى ذلك ، من الواضح لي أن اختباري يجب أن يكون قادرًا على تحرير الموارد في حالة حدوث استثناء ، كما لو كان رمزًا آمنًا عاديًا للاستثناءات.
من حيث المبدأ ، تبدو مكتبة tut-framework جيدة ، ولكن هناك بعض العيوب في تنفيذها. على سبيل المثال ، بالنسبة لحالتي ، أود ألا يكون للاختبار رقم فحسب ، بل وأيضًا سمات أخرى ، لا سيما الاسم ، بالإضافة إلى "حجم" الاختبار (على سبيل المثال ، هل هو اختبار تكامل أم أنه اختبار وحدة). يمكن حل هذا في إطار واجهة برمجة التطبيقات API ، وحتى وجود شيء ما بالفعل ، ويمكن تحقيق شيء ما إذا أضفت طريقة إلى واجهة برمجة التطبيقات للمكتبة واتصلت بها على نص الاختبار لتعيين أي من معلماتها:
template<> template<> void object::test<1>() { set_name("2+2");
مشكلة أخرى هي أن بيئة تشغيل اختبار tut لا تعرف شيئًا عن حدث مثل بداية الاختبار. تقوم البيئة بتنفيذ object::test<N>()
ولا تعرف مقدمًا ما إذا كان الاختبار قد تم تطبيقه على N معين أم أنه مجرد كعب روتين. إنها called_method_was_a_dummy_test_
فقط متى انتهى الاختبار من خلال تحليل القيمة التي called_method_was_a_dummy_test_
. لا تظهر هذه الميزة نفسها بشكل جيد للغاية في أنظمة CI ، والتي يمكنها تجميع المخرجات التي أجراها البرنامج بين بداية ونهاية الاختبار.
ومع ذلك ، في رأيي ، الشيء الرئيسي الذي يمكن تحسينه ("العيب القاتل") هو وجود كود إضافي إضافي مطلوب لكتابة الاختبارات. يوجد الكثير من الأشياء في tut-framework التعليمي: يُقترح أولاً إنشاء struct basic{}
فئة معينة struct basic{}
، ووصف الاختبارات بأنها طرق للكائن المرتبط بهذا. في هذا الفصل الدراسي ، يمكنك تحديد الأساليب والبيانات التي تريد استخدامها في مجموعة الاختبار ، ويقوم المُنشئ والمدمّر بإطار تنفيذ الاختبار ، مما يؤدي إلى إنشاء شيء مثل أداة التثبيت من jUnit. في ممارستي مع tut ، يكون هذا الكائن فارغًا دائمًا تقريبًا ، لكنه يمتد على طول عدد معين من أسطر التعليمات البرمجية.
لذلك ، نذهب إلى ورشة الدراجات ونحاول ترتيب الفكرة في شكل مكتبة صغيرة.
هذا هو ما يبدو عليه ملف الاختبار الأدنى في المكتبة المختبرة:
بالإضافة إلى نقص وحدات الماكرو ، تتمثل المكافأة في عدم استخدام الذاكرة الديناميكية داخل المكتبة.
تعريف حالات الاختبار
لتسجيل الاختبارات ، يتم استخدام السحر الابتدائي لمستوى الدخول على نفس مبدأ توت. في مكان ما في اختبار. هناك وظيفة من هذا النوع:
template <int N> static void Case(IRuntime* runtime) { throw TheCaseIsAStub(); }
حالات الاختبار المكتوبة من قبل مستخدمي المكتبة هي ببساطة تخصصات في هذه الطريقة. تم إعلان أن الوظيفة ثابتة ، أي في كل وحدة ترجمة ، نقوم بإنشاء تخصصات لا تتقاطع بالاسم مع بعضها البعض أثناء الارتباط.
هناك قاعدة تحتاج أولاً إلى استدعاء StartCase()
، والتي يمكنك من خلالها تمرير أشياء مثل اسم الاختبار وربما بعض الأشياء الأخرى التي لا تزال قيد التطوير.
عندما يستدعي الاختبار runtime->StartTest()
، يمكن أن تحدث أشياء مثيرة للاهتمام. أولاً ، إذا كانت الاختبارات الآن في وضع التشغيل ، فيمكنك معرفة مكان ما أن الاختبار قد بدأ التنفيذ. ثانياً ، إذا كان هناك وضع لجمع المعلومات حول الاختبارات المتاحة ، StartTest()
بإلقاء نوع خاص من الاستثناء يعني أن الاختبار حقيقي وليس كعب روتين.
تسجيل
في مرحلة ما ، تحتاج إلى جمع عناوين جميع حالات الاختبار ووضعها في مكان ما. في الاختبار ، يتم ذلك باستخدام المجموعات. يقوم مُنشئ الفئة :: Group المُختبرة بهذا كآثار جانبية:
static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
ينشئ المُنشئ مجموعة بالاسم المحدد ويضيف إليها جميع Case<N>
التي يعثر عليها في وحدة الترجمة الحالية. اتضح أنه في وحدة ترجمة واحدة لا يمكن أن يكون لديك مجموعتان. وهذا يعني أيضًا أنه لا يمكنك تقسيم مجموعة واحدة إلى عدة وحدات ترجمة.
معلمة القالب هي عدد حالات الاختبار التي يجب البحث عنها في وحدة الترجمة الحالية للمجموعة التي تم إنشاؤها.
رابط
في المثال أعلاه ، يحدث إنشاء الكائن :: Group () الذي تم اختباره داخل الوظيفة التي يجب أن نطلبها من تطبيقنا لتسجيل الاختبارات:
void LinkStdVectorTests() { static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); }
لا تكون الوظيفة مطلوبة دائمًا ، في بعض الأحيان يمكنك ببساطة إعلان كائن من فئة tested::Group
داخل ملف. ومع ذلك ، فإن تجربتي هي أن الرابط "يحسن" الملف بأكمله في بعض الأحيان إذا تم تجميعه داخل المكتبة ، ولا يستخدم أي من التطبيق الرئيسي أي أحرف من ملف cpp هذا:
calc.lib <- calc_test.lib(calc_test.cpp) ^ ^ | | app.exe run_test.exe
عندما لا يرتبط calc_test.cpp من مصدر run_test.exe ، فإن الرابط يزيل ببساطة هذا الملف من الاعتبار بالكامل ، إلى جانب إنشاء كائن ثابت ، على الرغم من أن له آثارًا جانبية نحتاجها.
إذا كانت أي سلسلة تنتج من run_test.exe ، فسيظهر الكائن الثابت في الملف القابل للتنفيذ. ولا يهم بالضبط كيف يتم ذلك ، كما في المثال:
void LinkStdVectorTests() { static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); }
او نحو ذلك:
static tested::Group<CASE_COUNTER> x("std.vector", __FILE__); void LinkStdVectorTests() { }
الخيار الأول ، في رأيي ، أفضل لأنه يتم استدعاء المنشئ بعد بداية main () ، ولديه التطبيق بعض التحكم في هذه العملية.
أعتقد أن هذا الإعداد للعكازات مطلوب لأي مكتبة اختبار وحدة تستخدم المتغيرات العامة والآثار الجانبية للمنشئ لإنشاء قاعدة بيانات اختبار. ومع ذلك ، يمكن تجنبها على الأرجح عن طريق ربط مكتبة الاختبار بالمفتاح - الأرشيف الكامل (ظهر التماثلية في MSVC في Visual Studio 2015.3 فقط).
وحدات الماكرو
لقد وعدت بأنه لن يكون هناك وحدات ماكرو ، لكنها - CASE_COUNTER
. خيار العمل هو أنه يتم استخدامه بواسطة __COUNTER__
، ماكرو يزيد المحول البرمجي بمقدار واحد في كل مرة يتم استخدامه داخل وحدة الترجمة.
بدعم من دول مجلس التعاون الخليجي ، CLANG ، MSVC ، ولكن ليس المعيار. إذا كان هذا الأمر محبطًا ، فإليك بعض البدائل:
- استخدم الأرقام 0 ، 1 ، 2
- استخدام معيار
__LINE__
. - استخدام constexpr السحر من مستوى 80. يمكنك البحث عن "constexpr counter" ومحاولة العثور على برنامج التحويل البرمجي الذي ستعمل عليه.
المشكلة في __LINE__
هي أن استخدام أرقام كبيرة في خيارات القالب يخلق حجم ملف قابل للتنفيذ كبير. هذا هو السبب في أنني قصرت نوع نمط char الموقَّع على 128 كحد أقصى لعدد الاختبارات في المجموعة.
فشل الذاكرة الديناميكية
اتضح أنه عند تسجيل الاختبارات ، لا يمكنك استخدام الذاكرة الديناميكية ، التي استخدمتها. من المحتمل أن بيئتك لا تحتوي على ذاكرة حيوية أو تستخدم البحث عن تسرب الذاكرة في حالات الاختبار ، وبالتالي فإن تدخل بيئة تنفيذ الاختبار ليس هو ما تحتاج إليه. Google Test يكافح مع هذا ، وهنا مقتطف من هناك:
ويمكننا ببساطة عدم خلق صعوبات.
كيف إذن نحصل على قائمة الاختبارات؟ هذه هي المزيد من المعلومات الداخلية الفنية ، والتي يسهل رؤيتها في التعليمات البرمجية المصدر ، لكنني سأخبرك على أي حال.
عند إنشاء مجموعة ، ستتلقى فئتها مؤشرًا إلى الدالة tested::CaseCollector<CASE_COUNTER>::collect
، والتي ستجمع كل اختبارات وحدة الترجمة في قائمة. إليك كيف تعمل:
اتضح أنه في كل وحدة ترجمة ، يتم إنشاء العديد من المتغيرات الثابتة لنوع CaseListEntry CaseCollector \ :: s_caseListEntry ، وهي عناصر قائمة الاختبار ، وتقوم طريقة collect () بجمع هذه العناصر في قائمة متصلة منفردة. بالطريقة نفسها تقريبًا ، تشكل القائمة مجموعات من الاختبارات ، ولكن بدون أنماط وتكرار.
هيكل
تحتاج الاختبارات إلى رابط مختلف ، مثل الإخراج إلى وحدة التحكم بالأحرف الحمراء الفاشلة ، وإنشاء تقارير اختبار بتنسيق يمكن فهمه لـ CI أو GUI حيث يمكنك رؤية قائمة من الاختبارات وتشغيل الاختبارات المحددة - بشكل عام ، الكثير من الأشياء. لدي رؤية لكيفية القيام بذلك ، وهو يختلف عن ما رأيته سابقًا في مكتبة الاختبارات. يتم المطالبة أساسًا بالمكتبات التي تطلق على نفسها اسم "الرأس فقط" ، مع تضمين مقدار كبير من الكود ، وهو في الأساس ليس لملفات الرأس.
النهج الذي أفترضه هو أننا نقسم المكتبة إلى واجهة أمامية - يتم اختبار هذا. ح ومكتبات خلفية. لكتابة الاختبارات ، تحتاج فقط إلى test.h ، والذي أصبح الآن C ++ 17 (بسبب std :: std :: string_view) ولكن من المفترض أنه سيكون هناك C ++ 98. يقوم Tested.h فعليًا بالتسجيل والبحث عن الاختبارات ، وهو خيار التشغيل الأقل ملائمة ، وكذلك القدرة على تصدير الاختبارات (المجموعات ، عناوين وظائف حالة الاختبار). يمكن للمكتبات الخلفية غير الموجودة بعد أن تفعل ما تشاء من حيث إخراج النتائج والتشغيل باستخدام وظيفة التصدير. بنفس الطريقة ، يمكنك تكييف الإطلاق مع احتياجات مشروعك.
ملخص
لا تزال المكتبة المختبرة ( كود github ) تحتاج إلى بعض الاستقرار. في المستقبل القريب ، أضف القدرة على تشغيل الاختبارات غير المتزامنة (اللازمة لاختبارات التكامل في WebAssembly) وتشير إلى حجم الاختبارات. في رأيي ، لا تزال المكتبة غير جاهزة تمامًا للاستخدام الإنتاجي ، لكنني أمضيت فجأة الكثير من الوقت وقد توقفت المرحلة وأخذت نفسًا وأطلب تعليقات من المجتمع. هل ترغب في استخدام هذا النوع من المكتبات؟ ربما هناك أي أفكار أخرى في ترسانة C ++ حيث سيكون من الممكن إنشاء مكتبة بدون وحدات ماكرو؟ هل مثل بيان المشكلة مثير للاهتمام على الإطلاق؟