اختبار وحدة C ++ وأنماط حقن وهمية باستخدام الصفات

مرحبا مرة أخرى! يتم ترك أقل من أسبوع قبل بدء الفصول في المجموعة في الدورة التدريبية "C ++ Developer" . في هذا الصدد ، نواصل تبادل المواد المفيدة المترجمة خصيصًا لطلاب هذه الدورة.



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

أدناه أعطت بعض الأمثلة مع تعقيد متزايد تقريبا لتنفيذ التعليمات البرمجية.

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

لنبدأ مع واحد بسيط.

يأخذ القالب وسيطة نوع وكائن من نفس النوع حسب المرجع في المُنشئ


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

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

على سبيل المثال:

template <class T> class TemplateUnderTest { T *t_; public: TemplateUnderTest(T *t) : t_(t) {} void SomeMethod() { t->DoSomething(); t->DoSomeOtherThing(); } }; struct MockT { void DoSomething() { // Some assertions here. } void DoSomeOtherThing() { // Some more assertions here. } }; class UnitTest { void Test1() { MockT mock; TemplateUnderTest<MockT> test(&mock); test.SomeMethod(); assert(DoSomethingWasCalled(mock)); assert(DoSomeOtherThingWasCalled(mock)); } }; 


القالب يأخذ وسيطة نوع. عمل نسخة من وسيطة المنشئ أو ببساطة لا يقبلها


في هذه الحالة ، قد لا يكون الوصول إلى الكائن داخل القالب ممكنًا بسبب حقوق الوصول. يمكنك استخدام فئات friend .

 template <class T> class TemplateUnderTest { T t_; friend class UnitTest; public: void SomeMethod() { t.DoSomething(); t.DoSomeOtherThing(); } }; class UnitTest { void Test2() { TemplateUnderTest<MockT> test; test.SomeMethod(); assert(DoSomethingWasCalled(test.t_)); // access guts assert(DoSomeOtherThingWasCalled(test.t_)); // access guts } }; 

UnitTest :: Test2 لديه حق الوصول إلى نص TemplateUnderTest ويمكن التحقق من البيانات على النسخة الداخلية من MockT.

يأخذ القالب وسيطة الكتابة ويقوم بإنشاء عدة قوالب مترابطة بدون وظائف افتراضية


في هذه الحالة ، سألقي نظرة على مثال في العالم الحقيقي: Google RPC غير متزامن .

في C ++ ، يحتوي gRPC غير المتزامن على شيء يسمى CallData ، والذي ، كما يوحي الاسم ، يخزن البيانات المتعلقة باستدعاء RPC . يمكن لقالب CallData معالجة عدة أنواع مختلفة من RPCs. لذلك فمن الطبيعي أن يتم تنفيذه بدقة من قبل القالب.

يقبل CallData عام وسيطات نوع اثنين: طلب واستجابة. يمكن أن يبدو مثل هذا:

 template <class Request, class Response> class CallData { grpc::ServerCompletionQueue *cq_; grpc::ServerContext context_; grpc::ServerAsyncResponseWriter<Response> responder_; // ... some more state public: using RequestType = Request; using ResponseType = Response; CallData(grpc::ServerCompletionQueue *q) : cq_(q), responder_(&context_) {} void HandleRequest(Request *req); // application-specific code Response *GetResponse(); // application-specific code }; 

يجب أن يتحقق اختبار الوحدة لقالب CallData من سلوك HandleRequest و HandleResponse. تستدعي هذه الوظائف عددًا من وظائف الأعضاء. لذلك ، يعد التحقق من صحة مكالمتهم أمرًا بالغ الأهمية لصحة CallData. ومع ذلك ، هناك الحيل.

  1. يتم إنشاء بعض الأنواع من مساحة اسم grpc داخليًا ولا يتم تمريرها عبر المنشئ. ServerAsyncResponseWriter و ServerContext ، على سبيل المثال.
  2. يتم تمرير grpc :: ServerCompletionQueue إلى المُنشئ كوسيطة ، ولكن لا يوجد لديه وظائف افتراضية. فقط المدمر الظاهري.
  3. يتم إنشاء grpc :: ServerContext داخليًا وليس له وظائف ظاهرية.

والسؤال هو كيفية اختبار CallData دون استخدام gRPC الكامل في الاختبارات؟ كيفية محاكاة ServerCompletionQueue؟ كيفية محاكاة ServerAsyncResponseWriter ، وهو بحد ذاته قالب؟ وهلم جرا ...

بدون وظائف افتراضية ، يصبح استبدال سلوك المستخدم مهمة معقدة. لا يمكن نمذجة الأنواع ذات الترميز الثابت ، مثل grpc :: ServerAsyncResponseWriter ، لأنها hmm و hardcoded وغير مطبقة.

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

إذن ماذا نفعل؟

الحل: الصفات




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

النظر في CallDataTraits

 template <class CallData> class CallDataTraits { using ServerCompletionQueue = grpc::ServerCompletionQueue; using ServerContext = grpc::ServerContext; using ServerAsyncResponseWriter = grpc::ServerAsyncResponseWrite<typename CallData::ResponseType>; }; 

هذا هو القالب الرئيسي للسمات المستخدمة لرمز الإنتاج. لنستخدمها في CallDatatemplate.

 /// Unit testable CallData template <class Request, class Response> class CallData { typename CallDataTraits<CallData>::ServerCompletionQueue *cq_; typename CallDataTraits<CallData>::ServerContext context_; typename CallDataTraits<CallData>::ServerAsyncResponseWriter responder_; // ... some more state public: using RequestType = Request; using ResponseType = Response; CallData(typename CallDataTraits::ServerCompletionQueue *q) : cq_(q), responder_(&context_) {} void HandleRequest(Request *req); // application-specific code Response *GetResponse(); // application-specific code }; 

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

 /// In unit test code struct TestRequest{}; struct TestResponse{}; struct MockServerCompletionQueue{}; struct MockServerContext{}; struct MockServerAsyncResponseWriter{}; /// We want to unit test this type. using CallDataUnderTest = CallData<TestRequest, TestResponse>; /// A specialization of CallDataTraits for unit testing purposes only. template <> class CallDataTraits<CallDataUnderTest> { using ServerCompletionQueue = MockServerCompletionQueue; using ServerContext = MockServerContext; using ServerAsyncResponseWriter = MockServerAsyncResponseWrite; }; MockServerCompletionQueue mock_queue; CallDataUnderTest cdut(&mock_queue); // Now injected with mock types. 

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

كيف تحب المواد؟ اكتب التعليقات. ونراكم عند الباب المفتوح ؛-)

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


All Articles