أصبح الاختبار أسهل من خلال بساطتها الهيكلية وهندسة البرمجيات

نشرت: 2022-03-10
ملخص سريع ↬ كما هو الحال مع العديد من الموضوعات الأخرى في تطوير البرامج ، غالبًا ما يكون التطوير القائم على الاختبار والاختبار معقدًا بلا داعٍ من الناحية النظرية والتنفيذ من خلال التركيز بشكل كبير على تعلم مجموعة واسعة من أطر الاختبار. في هذه المقالة ، سنعيد النظر في ما يعنيه الاختبار من خلال تشبيه بسيط ، ونستكشف المفاهيم في بنية البرامج التي ستؤدي مباشرةً إلى انخفاض الحاجة إلى أطر عمل الاختبار ، وبعض الحجج حول سبب استفادتك من أسلوب التبسيط في عملية الاختبار الخاصة بك .

مثل العديد من مطوري Android الآخرين ، قادني تجربتي الأولية للاختبار على النظام الأساسي إلى أن أواجه على الفور درجة معنوية من المصطلحات. علاوة على ذلك ، فإن الأمثلة القليلة التي صادفتها في ذلك الوقت (حوالي عام 2015) لم تقدم حالات استخدام عملية ربما دفعتني إلى الاعتقاد بأن نسبة التكلفة إلى الفائدة لتعلم أداة مثل Espresso من أجل التحقق من أن TextView.setText ( …) كان يعمل بشكل صحيح ، كان استثمارًا معقولًا.

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

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

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

المزيد بعد القفز! أكمل القراءة أدناه ↓

الاختبار: لماذا وكيف

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

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

تشير "المرحلة الأولى" إلى المعززات التي يتم إطلاقها عند إطلاق الصاروخ لأول مرة

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

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

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

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

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

بخصوص البرمجيات

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

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

لماذا نحتاج إلى أطر عمل؟

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

 class Calculator { private int add(int a, int b){ return a + b; } private int subtract(int a, int b){ return a - b; } }

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

 public class Main { public static void main(String[] args){ //create a copy of the Unit to be tested Calculator calc = new Calculator(); //create test conditions to verify behaviour int addTest = calc.add(2, 2); int subtractTest = calc.subtract(2, 2); //verify behaviour by assertion if (addTest == 4) System.out.println("addTest has passed."); else System.out.println("addTest has failed."); if (subtractTest == 0) System.out.println("subtractTest has passed."); else System.out.println("subtractTest has failed."); } }

يعد اختبار تطبيق Android بالطبع إجراءً مختلفًا تمامًا. على الرغم من وجود وظيفة main مدفونة بعمق داخل مصدر ملف ZygoteInit.java (التفاصيل الدقيقة التي ليست مهمة هنا) ، والتي يتم استدعاؤها قبل تشغيل تطبيق Android على JVM ، حتى مطور Android المبتدئ يجب أن تعرف أن النظام نفسه مسؤول عن استدعاء هذه الوظيفة ؛ ليس المطور . بدلاً من ذلك ، تكون نقاط الدخول لتطبيقات Android هي فئة Application وأي فئات Activity يمكن الإشارة إلى النظام إليها عبر ملف AndroidManifest.xml .

كل هذا مجرد مقدمة لحقيقة أن وحدات الاختبار في تطبيق Android تقدم مستوى أعلى من التعقيد ، لأن بيئة الاختبار لدينا يجب أن تمثل الآن نظام Android الأساسي.

ترويض مشكلة الاقتران الضيق

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

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

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

حل آخر لمشكلة الاقتران المحكم ، هو استخدام أطر عمل الاختبار للتفاعل مع ، أو محاكاة (محاكاة) تبعيات النظام الأساسي. توفر الأطر مثل Espresso و Robolectric للمطورين وسائل أكثر فاعلية لاختبار الوحدات مقارنة بالنهج السابق ؛ يُعد الأول مفيدًا للاختبارات التي يتم إجراؤها على جهاز (يُعرف باسم "الاختبارات الآلية" لأنه من الواضح أن استدعاءها لاختبارات الجهاز لم يكن غامضًا بدرجة كافية) والأخير قادر على السخرية من إطار عمل Android محليًا على JVM.

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

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

حافظ على الأطر الخاصة بك على طول الذراعين

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

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

من الناحية الفنية ، لا يمكن أن تعمل أطر عمل مثل Espresso وبدرجة أقل Robolectric بكفاءة اختبارات JUnit البسيطة ، أو حتى اختبار الإطار المجاني من وقت سابق. في حين أن JUnit هي بالفعل إطار عمل ، إلا أنها مرتبطة بإحكام بـ JVM ، والتي تميل إلى التغيير بمعدل أبطأ بكثير من نظام Android الأساسي المناسب. عدد أقل من الأطر يعني دائمًا رمزًا أكثر كفاءة من حيث الوقت الذي يستغرقه تنفيذ وكتابة اختبار واحد أو أكثر.

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

فن العمارة

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

فصل المخاوف

إن فصل الاهتمامات هو حسب تقديري المفهوم الأكثر قابلية للتطبيق والمفيد عالميًا في هندسة البرمجيات ككل (بدون معنى القول بأنه يجب إهمال الآخرين). يمكن تطبيق فصل الاهتمامات (SOC) ، أو تجاهله تمامًا ، عبر كل منظور من منظور تطوير البرامج الذي أعرفه. لتلخيص المفهوم بإيجاز ، سنلقي نظرة على SOC عند تطبيقه على الفصول ، ولكن كن مدركًا أنه يمكن تطبيق SOC على الوظائف من خلال الاستخدام المكثف للوظائف المساعدة ، ويمكن استقراءها في الوحدات النمطية الكاملة للتطبيق ("الوحدات النمطية" المستخدمة في سياق Android / Gradle).

إذا كنت قد قضيت وقتًا طويلاً في البحث عن أنماط معمارية للبرامج لتطبيقات واجهة المستخدم الرسومية ، فمن المحتمل أن تكون قد صادفت واحدًا على الأقل من: Model-View-Controller (MVC) أو Model-View-Presenter (MVP) أو Model-View- ViewModel (MVVM). بعد إنشاء تطبيقات في كل نمط ، سأقول مقدمًا أنني لا أعتبر أيًا منها الخيار الأفضل الوحيد لجميع المشاريع (أو حتى الميزات الموجودة في مشروع واحد). ومن المفارقات ، أن النمط الذي قدمه فريق Android منذ بضع سنوات كنهج موصى به ، MVVM ، يبدو أنه الأقل قابلية للاختبار في غياب أطر اختبار خاصة بنظام Android (على افتراض أنك ترغب في استخدام فئات ViewModel لمنصة Android ، والتي من المسلم بها أنني معجب بها من).

على أي حال ، فإن تفاصيل هذه الأنماط أقل أهمية من عمومياتها. كل هذه الأنماط هي مجرد نكهات مختلفة لـ SOC والتي تؤكد على الفصل الأساسي لثلاثة أنواع من الكود الذي أشير إليه على النحو التالي: البيانات ، واجهة المستخدم ، المنطق .

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

سيدير ​​الفصل الأول واجهة المستخدم الخاصة بنا. لتبسيط الأمور ، استخدمت نشاطًا لهذا الغرض ، لكنني عادةً ما أختار Fragments بدلاً من ذلك كفئات لواجهة المستخدم. في كلتا الحالتين ، تقدم كلا الفئتين اقترانًا وثيقًا مشابهًا لمنصة Android :

 public class CalculatorUserInterface extends Activity implements CalculatorContract.IUserInterface { private TextView display; private CalculatorContract.IControlLogic controlLogic; private final String INVALID_MESSAGE = "Invalid Expression."; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); controlLogic = new DependencyProvider().provideControlLogic(this); display = findViewById(R.id.textViewDisplay); Button evaluate = findViewById(R.id.buttonEvaluate); evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } }); //..bindings for the rest of the calculator buttons } @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } }

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

اختبار منطق التحكم

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

 public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } } public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } }

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

لاحظ أيضًا أنه في دالة evaluateExpression() ، سأعيد فئة من النوع Optional<String> من النهاية الخلفية. عادةً ما أستخدم ما يسميه المبرمجون الوظيفيون إما Monad ، أو كما أفضل أن أطلق عليه ، غلاف النتيجة . مهما كان الاسم الغبي الذي تستخدمه ، فهو كائن قادر على تمثيل حالات مختلفة متعددة من خلال استدعاء وظيفة واحدة. الخيار Optional هو بناء أبسط يمكن أن يمثل قيمة فارغة أو قيمة معينة من النوع العام المزود. على أي حال ، نظرًا لأنه قد يتم إعطاء النهاية الخلفية تعبيرًا غير صالح ، فإننا نريد أن نعطي فئة ControlLogic بعض الوسائل لتحديد نتيجة عملية الواجهة الخلفية ؛ المحاسبة عن كل من النجاح والفشل. في هذه الحالة ، سوف تمثل القيمة الفارغة فشلًا.

يوجد أدناه مثال لفئة اختبار تمت كتابتها باستخدام JUnit ، والفصل الذي يُطلق عليه في اختبار المصطلحات فئة Fake :

 public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } } public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } }

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

قوة التجريد وانقلاب التبعية

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

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

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

اختبار منطق الحساب

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

 public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }

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

 public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }

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

كائنات متواضعة ووجهات نظر سلبية

يشير الاسمان أعلاه إلى نمط يتم فيه تبسيط الكائن الذي يجب أن يتحدث إلى تبعيات منخفضة المستوى لدرجة أنه لا يحتاج إلى اختبار. لقد تعرفت على هذا النمط لأول مرة عبر مدونة Martin Fowler حول الاختلافات في Model-View-Presenter. لاحقًا ، من خلال أعمال Robert C. ضمنيًا مثل هذا القيد).

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

 //from CalculatorActivity's onCreate() function: evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } });

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

 //Interface functions of CalculatorActivity: @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } //…

In principal, these two examples ought to give you enough to understand how to go about implementing this pattern. The object which possesses the logic is loosely coupled, and the object which is tightly coupled to pesky dependencies becomes almost devoid of logic.

Now, at the start of this subsection, I made the statement that these classes become arguably unnecessary to test, and it is important we look at both sides of this argument. In an absolute sense, it is impossible to achieve 100% test coverage by employing this pattern, unless you still write tests for such humble / passive classes. It is also worth noting that my decision to use a Calculator as an example App, means that I cannot escape having a gigantic mass of findViewById(...) calls present in the Activity. Giant masses of repetitive code are a common cause of typing errors, and in the absence of some Android UI testing frameworks, my only recourse for testing would be via deploying the feature to a device and manually testing each interaction. أوتش.

It is at this point that I will humbly say that I do not know if 100% code coverage is absolutely necessary. I do not know many developers who strive for absolute test coverage in production code, and I have never done so myself. One day I might, but I will reserve my opinions on this matter until I have the reference experiences to back them up. In any case, I would argue that applying this pattern will still ultimately make it simpler and easier to test tightly coupled classes; if for no reason other than they become simpler to write.

Another objection to this approach, was raised by a fellow programmer when I described this approach in another context. The objection was that the logic class (whether it be a Controller , Presenter , or even a ViewModel depending on how you use it), becomes a God class.

While I do not agree with that sentiment, I do agree that the end result of applying this pattern is that your Logic classes become larger than if you left more decisions up to your user interface class.

This has never been an issue for me as I treat each feature of my applications as self-contained components, as opposed to having one giant controller for managing multiple user interface screens. In any case, I think this argument holds reasonably true if you fail to apply SOC to your front end or back end components. Therefore, my advice is to apply SOC to your front end and back end components quite rigorously.

Further Considerations

After all of this discussion on applying the principles of software architecture to reduce the necessity of using a wide-array of testing frameworks, improve the testability of classes in general, and a pattern which allows classes to be tested indirectly (at least to some degree), I am not actually here to tell you to stop using your preferred frameworks.

For those curious, I often use a library to generate mock classes for my Unit tests (for Java I prefer Mockito , but these days I mostly write Kotlin and prefer Mockk in that language), and JUnit is a framework which I use quite invariably. Since all of these options are coupled to languages as opposed to the Android platform, I can use them quite interchangeably across mobile and web application development. From time to time (if project requirements demand it), I will even use tools like Robolectric , MockWebServer , and in my five years of studying Android, I did begrudgingly use Espresso once.

My hope is that in reading this article, anyone who has experienced a similar degree of aversion to testing due to paralysis by jargon analysis , will come to see that getting started with testing really can be simple and framework minimal .

مزيد من القراءة على SmashingMag:

  • Sliding In And Out Of Vue.js
  • Designing And Building A Progressive Web Application Without A Framework
  • أطر CSS أو CSS Grid: ما الذي يجب أن أستخدمه لمشروعي؟
  • استخدام Flutter من Google لتطوير الأجهزة المحمولة عبر الأنظمة الأساسية حقًا