مقدّمة عن اختبار الضعف والحقنة الاعتمادية

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

مقدمة

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

  • اختبارات وحدة المستودع
  • اختبارات الدمج ونموذج العرض
  • اختبارات التنقل المجزأة

ما يجب معرفته

ويجب أن تكون على دراية بما يلي:

ما ستتعرَّف عليه

  • كيفية التخطيط لاستراتيجية اختبار
  • كيفية إنشاء واستخدام زوجيات الاختبار، أي المنتجات المزيفة والزائفة
  • كيفية استخدام إدخال الاعتمادية على Android لاختبارات الوحدات والدمج
  • كيفية تطبيق نمط محدّد مواقع الخدمات
  • كيفية اختبار المستودعات والأجزاء ونماذج العرض ومكوّن التنقل

وستستخدم المكتبات ومفاهيم الرموز التالية:

الإجراءات التي ستنفذّها

  • كتابة اختبارات الوحدات للمستودع باستخدام اختبار التبعية المزدوج والاعتمادية.
  • كتابة اختبارات الوحدات لنموذج العرض باستخدام اختبار مزدوج والاعتمادية.
  • كتابة اختبارات الدمج للأجزاء ونماذج العرض باستخدام إطار عمل اختبار واجهة مستخدم Espresso.
  • اكتب اختبارات التنقل باستخدام Mockito وEspresso.

في هذه السلسلة من الدروس التطبيقية حول الترميز، ستعمل مع تطبيق الملاحظات في قائمة المهام. ويتيح لك التطبيق تدوين المهام لإكمالها وعرضها في قائمة. ويمكنك بعد ذلك وضع علامة "مكتملة" أو "غير مكتملة" أو فلترتها أو حذفها.

هذا التطبيق مكتوب بلغة Kotlin ويحتوي على بعض الشاشات ويستخدم مكونات Jetpack ويتّبع البنية من دليل هندسة التطبيقات. من خلال تعلّم كيفية اختبار هذا التطبيق، ستتمكّن من اختبار التطبيقات التي تستخدم المكتبات والبنية نفسها.

تنزيل الرمز

للبدء، يُرجى تنزيل الرمز:

تنزيل ملف Zip

بدلاً من ذلك، يمكنك إنشاء نسخة طبق الأصل من مستودع Github للرمز:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

يُرجى تخصيص بعض الوقت للتعرّف على الرمز، باتّباع التعليمات الواردة أدناه.

الخطوة 1: تشغيل نموذج التطبيق

بعد تنزيل تطبيق المهام المطلوبة، افتحه في Android Studio وشغِّله. يجب أن يتم تجميعها. استكشِف التطبيق من خلال إجراء ما يلي:

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

الخطوة 2: استكشاف نموذج رمز التطبيق

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

من المهم فهم البنية العامة للتطبيق بدلاً من فهم عميق للمنطق في أي طبقة.

في ما يلي ملخص الحِزم التي ستظهر لك:

الحزمة: com.example.android.architecture.blueprints.todoapp

.addedittask

إضافة شاشة مهمة أو تعديلها: رمز طبقة واجهة المستخدم لإضافة مهمة أو تعديلها.

.data

طبقة البيانات: تتعامل هذه الطبقة مع طبقة بيانات المهام. ويحتوي هذا المورد على قاعدة البيانات والشبكة ورمز المستودع.

.statistics

شاشة الإحصاءات: رمز طبقة واجهة المستخدِم لشاشة الإحصاءات.

.taskdetail

شاشة تفاصيل المهمة: رمز طبقة واجهة المستخدم لمَهمة واحدة.

.tasks

شاشة المهام: رمز طبقة واجهة المستخدم لقائمة جميع المهام.

.util

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

طبقة البيانات (data.)

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

إحداثيات DefaultTasksRepository أو وسيطات بين طبقة الاتصال وطبقة قاعدة البيانات وهي ما يعرض البيانات إلى طبقة واجهة المستخدم.

طبقة واجهة المستخدم ( .addedittask, .statistics, .taskdetail, .tasks)

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

التنقل

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

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

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

هرم الاختبار

عند التفكير في استراتيجية للاختبار، هناك ثلاثة جوانب ذات صلة للاختبار:

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

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

  • اختبارات الوحدة: هذه اختبارات عالية التركيز يتم تنفيذها على صف واحد، وعادةً ما تكون طريقة واحدة في ذلك الصف. إذا تعذّر اختبار الوحدة، يمكنك معرفة مكان المشكلة في الرمز بالضبط. نظرًا لدقّتها المنخفضة في الواقع، يتضمّن تطبيقك أكثر من تنفيذ طريقة أو فئة واحدة. وهي سريعة بما يكفي للتشغيل في كل مرة تغير فيها الرمز. وغالبًا ما يتم إجراء الاختبارات محليًا (في مجموعة مصادر test). مثال: اختبار طرق فردية في نماذج العرض والمستودعات.
  • اختبارات الدمج - تعمل هذه على اختبار التفاعل لعدة صفوف للتأكُّد من أنها تعمل بالشكل المتوقع عند استخدامها معًا. وتتمثل إحدى طرق تنظيم اختبارات الدمج في اختبار ميزة واحدة، مثل القدرة على حفظ المهمة. تختبر هذه الاختبارات نطاقًا أوسع من الترميز مقارنةً باختبارات الوحدة، ولكنها لا تزال مُحسَّنة للتشغيل بسرعة، مقارنةً بالدقة الكاملة. ويمكن تنفيذها محليًا أو كاختبارات على قياس الأداء، اعتمادًا على الحالة. مثال: اختبار جميع وظائف جزء واحد وعرض زوج النماذج.
  • الاختبارات النهائية (E2e): يمكنك اختبار تركيبة من الميزات التي تعمل معًا. وتختبر هذه الميزة أجزاءً كبيرة من التطبيق وتحاكي الاستخدام الفعلي عن كثب، وبالتالي تكون عادةً بطيئة. وتتميّز هذه التطبيقات بأعلى مستوى من الدقة، وتخبرك بأن تطبيقك يعمل بشكل عام. على نطاق واسع، سيتم إجراء هذه الاختبارات على شكل اختبارات (في مجموعة المصدر androidTest)
    مثال: بدء تشغيل التطبيق بالكامل واختبار بعض الميزات معًا.

وتمثّل النسبة المقترَحة من هذه الاختبارات في الغالب هرمًا تمثّل غالبية اختباراتها الغالبية العظمى.

الهندسة المعمارية والاختبار

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

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



في هذا الدرس، ستتعرّف على كيفية اختبار أجزاء من البنية أعلاه باستخدام عزل مناسب:

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

وسيتم تناول الاختبارات الشاملة في الدرس التالي.

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

قد يكون اختبار الرمز فقط في صف أو صفوف معينة أمرًا صعبًا. لنلقِ نظرة على أحد الأمثلة. افتح الصف data.source.DefaultTaskRepository في مجموعة المصادر main. هذا هو مستودع التطبيق، وهو الصفّ الذي ستكتب اختبارات الوحدات له تاليًا.

وهدفك هو اختبار الرمز في ذلك الصف فقط. ومع ذلك، يعتمد DefaultTaskRepository على الفئات الأخرى، مثل LocalTaskDataSource وRemoteTaskDataSource، ليعمل. وإحدى الطرق الأخرى للتعبير عن ذلك هي أن LocalTaskDataSource وRemoteTaskDataSource يمثّلان تبعيات لكلٍّ من DefaultTaskRepository.

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



على سبيل المثال، يمكنك الاطّلاع على هذه الطريقة في DefaultTasksRepo.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks هي واحدة من أكثر المكالمات &الأساسية&عرضة، التي يمكنك إجراؤها في مستودعك. وتشمل هذه الطريقة القراءة من قاعدة بيانات SQLite وإجراء استدعاءات الشبكة (استدعاء إلى updateTasksFromRemoteDataSource). ويشمل ذلك رمزًا أكثر من رمز المستودع.

إليك بعض الأسباب الأكثر تحديدًا وراء صعوبة اختبار المستودع:

  • عليك التفكير في إنشاء قاعدة بيانات وإدارتها لتنفيذ أبسط الاختبارات في هذا المستودع. يؤدي هذا إلى ظهور أسئلة مثل "&quot؛ وما إذا كان هذا اختبارًا محليًا أو أداة اختبار، وإذا كان عليك استخدام اختبار AndroidX للحصول على بيئة Android افتراضية.
  • قد يستغرق تشغيل بعض أجزاء من الرمز، مثل رمز الشبكة، وقتًا طويلاً، أو قد يتعذّر أحيانًا إتمامها، ما يؤدي إلى إجراء اختبارات غير مستقرة لمدة طويلة.
  • قد تفقد اختباراتك قدرتها على تحديد الرمز الذي يخطئ في حال تعذّر إجراء الاختبار. قد تبدأ اختباراتك باختبار رمز غير مستودعي، لذا على سبيل المثال، قد يتعذّر إجراء اختبارات الوحدات المُفترضة والتعبيرية عن مشكلة بسبب مشكلة في بعض الرموز التابعة، مثل رمز قاعدة البيانات.

اختبار زوجي

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

في ما يلي بعض أنواع الاختبارات المزدوجة:

تزييف

اختبار مزدوج يحتوي على ن&ظارة "عمل"؛ وتنفيذ؛ للصف، ولكن تم تنفيذه بطريقة تجعله مناسبًا للاختبارات ولكنه غير مناسب للإنتاج.

المحاكاة

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

رمز بديل

اختبار ثنائي لا يتضمن أي منطق ولا يعرض سوى ما تتم برمجته من أجل عرضه. يمكن مثلاً برمجة StubTaskRepository لعرض مجموعات معيّنة من المهام من getTasks.

دمية

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

جاسوس

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

للحصول على مزيد من المعلومات حول الاختبارات المزدوجة، اطّلِع على الاختبار على المرحاض: التعرّف على زوجي في اختبارك.

يتمثل الاختبار المزدوج الأكثر استخدامًا في Android في Fakes وMocks.

في هذه المهمة، ستنشئ اختبار FakeDataSource مزدوجًا لاختبار الوحدة DefaultTasksRepository منفصلاً عن مصادر البيانات الفعلية.

الخطوة 1: إنشاء صف FakeDataSource

في هذه الخطوة، ستنشئ صفًا باسم FakeDataSouce، والذي سيكون اختبارًا لمضاعفة LocalDataSource وRemoteDataSource.

  1. في مجموعة المصدر test، انقر بزر الماوس الأيمن على New -> Package.

  1. أنشئ حزمة data بحيث تحتوي على حزمة source.
  2. أنشئ صفًا جديدًا باسم FakeDataSource في حزمة data/source.

الخطوة 2: تنفيذ واجهة مهام DataData

لكي تتمكّن من استخدام الصف الجديد FakeDataSource كاختبار مزدوج، يجب أن يكون بإمكانه استبدال مصادر البيانات الأخرى. مصادر البيانات هذه هي TasksLocalDataSource وTasksRemoteDataSource.

  1. لاحِظ كيف تنفِّذ هاتان الميزتان واجهة TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. تنفيذ FakeDataSource: TasksDataSource
class FakeDataSource : TasksDataSource {

}

سيشتكي استوديو Android من عدم تنفيذ الطرق المطلوبة للتطبيق TasksDataSource.

  1. استخدم قائمة الإصلاح السريع واختر تنفيذ الأعضاء.


  1. اختَر جميع الطرق واضغط على حسنًا.

الخطوة 3: تنفيذ طريقة getTasks في FakeDataSource

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

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

FakeDataSource

  • يتيح لك اختبار الرمز في DefaultTasksRepository بدون الحاجة إلى الاعتماد على قاعدة بيانات أو شبكة حقيقية.
  • ويوفّر تنفيذاً حقيقيًا&قدرًا كافيًا من الاختبارات للاختبارات.
  1. يمكنك تغيير طريقة إنشاء FakeDataSource لإنشاء var باسم tasks وهو MutableList<Task>? مع قيمة تلقائية لقائمة متغيّرات فارغة.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


هذه هي قائمة المهام التي &&كثيرة&كثيرة، والتي تمثل قاعدة بيانات أو استجابة الخادم. والهدف الحالي هو اختبار طريقة repository's getTasks. ويؤدي ذلك إلى استدعاء الأساليب source data's getTasks وdeleteAllTasks وsaveTask.

كتابة نسخة مزيفة من الطرق التالية:

  1. اكتب getTasks: إذا لم يكن tasks هو null، يمكنك عرض نتيجة Success. إذا كانت قيمة tasks هي null، يتم عرض نتيجة Error.
  2. كتابة deleteAllTasks: محو قائمة المهام القابلة للتغيير.
  3. كتابة saveTask: إضافة المهمة إلى القائمة.

تبدو هذه الطرق، التي تم تنفيذها من أجل FakeDataSource، تشبه الرمز الوارد أدناه.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

في ما يلي كشوفات الاستيراد إذا لزم الأمر:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

ويماثل هذا طريقة عمل مصادر البيانات المحلية والبعيدة الفعلية.

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

وتكمن المشكلة الرئيسية في أنّه لديك FakeDataSource، إلا أنّه لا يتوفّر معلومات واضحة عن كيفية استخدامه في الاختبارات. يجب استبدال TasksRemoteDataSource وTasksLocalDataSource، ولكن فقط في الاختبارات. كل من TasksRemoteDataSource وTasksLocalDataSource يعتمدان على DefaultTasksRepository، ما يعني أن DefaultTasksRepositories يتطلب أو يعتمد عليهما عرض الأسعار على هذه الصفوف.

في الوقت الحالي، يتم إنشاء الاعتماديات داخل طريقة init من DefaultTasksRepository.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

نظرًا لأنك تنشئ taskLocalDataSource وtasksRemoteDataSource وتعيّنهما داخل DefaultTasksRepository، فإنهما أساسيان. لا يمكن التبديل في الاختبار المزدوج.

بدلاً من ذلك، وفِّر مصادر البيانات هذه للصف، بدلاً من ترميزها بقوة. يُعرف توفير العناصر التابعة لها باسم إدخال التبعية. هناك طرق مختلفة لتوفير التبعية، وبالتالي هناك أنواع مختلفة من أنواع الاعتمادية.

يسمح لك إدخال التبعية للمُنشئ بالتبديل في الاختبار مرّتين من خلال تمريره إلى المُنشئ.

بلا إدخال

حقن

الخطوة 1: استخدام إدخال الاعتماد على المُنشأة في DefaultTasksRepository

  1. غيِّر طريقة وضع عامل التشغيل DefaultTaskRepository' من إدخال Application في مصدر البيانات ومُرسل الكوروتين (الذي ستحتاج إليه أيضًا في مقابله للاختبارات - تم توضيح ذلك بالتفصيل في قسم الدرس الثالث حول الكوروتين).

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. بما أنّك استخدمت روابط التبعية بشكل صحيح، عليك إزالة الطريقة init. لم تعد بحاجة إلى إنشاء تبعيات.
  2. احذف أيضًا متغيرات المثيل القديم. أنت تحدّد هذه العناصر في طريقة الإنشاء:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. أخيرًا، عدِّل طريقة getRepository لاستخدام طريقة الإنشاء الجديدة:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

أنت الآن تستخدم حقن الاعتمادات الإنشائية.

الخطوة 2: استخدام FakeDataSource في اختباراتك

الآن وبعد أن تلقّى الرمز الخاص بك إدخال تبعية للرمز، يمكنك استخدام مصدر البيانات الزائف لاختبار DefaultTasksRepository.

  1. انقر بزر الماوس الأيمن على اسم الفئة في DefaultTasksRepository واختَر إنشاء، ثم اختبار.
  2. اتّبع رسائل المطالبة لإنشاء DefaultTasksRepositoryTest في مجموعة المصادر التجريبية.
  3. في أعلى صف DefaultTasksRepositoryTest الجديد، أضِف متغيّرات العضو أدناه لتمثيل البيانات في مصادر البيانات المزيفة.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. أنشئ ثلاثة متغيرات، وFakeDataSource متغير عضو (واحد لكل مصدر بيانات لمستودعك) ومتغير لـ DefaultTasksRepository الذي ستختبره.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

اختَر طريقة لإعداد DefaultTasksRepository قابلة للاختبار. سيستخدم DefaultTasksRepository هذا الاختبار، FakeDataSource.

  1. إنشاء طريقة باسم createRepository وإضافة تعليقات توضيحية إليها باستخدام @Before.
  2. أنشئ مصادر بيانات زائفة، باستخدام قائمتي remoteTasks وlocalTasks.
  3. عليك إنشاء مثيل لـ tasksRepository، باستخدام مصدري البيانات الزائفين اللذين أنشأتهما للتو وDispatchers.Unconfined.

من المفترض أن تبدو الطريقة النهائية مثل الرمز الوارد أدناه.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

الخطوة 3: كتابة DefaultTasksRepository getTasks() Test

حان الوقت لكتابة اختبار DefaultTasksRepository.

  1. اكتب اختبارًا لطريقة getTasks في المستودع. تحقق عند استدعاء getTasks باستخدام true (بمعنى أنه يجب إعادة التحميل من مصدر البيانات عن بُعد) فهو يعرض بيانات من مصدر البيانات البعيد (بدلاً من مصدر البيانات المحلي).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

ستظهر لك رسالة خطأ عند الاتصال بالرقم getTasks:.

الخطوة 4: إضافة RunBlockTest

يُتوقَّع حدوث خطأ كورونين لأن getTasks هي دالة suspend وعليك تشغيل كوروتين لاستدعاءه. لِتَنْفِيذْ هَذَا الْإِجْرَاءْ، يَجِبُ تَوْفِيرْ مِنْطَقَة كُرْيُوتِينْ. لحل هذا الخطأ، ستحتاج إلى إضافة بعض تبعيات الدرجات لمعالجة الكوروتينات في اختباراتك.

  1. أضِف التبعيات المطلوبة لاختبار الكوروتينات إلى مصدر الاختبار الذي تم ضبطه باستخدام testImplementation.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

لا تنسَ المزامنة!

kotlinx-coroutines-test هي مكتبة اختبار الكوروتين، وهي مخصّصة خصيصًا لاختبار الكوروتين. لإجراء اختباراتك، استخدِم الوظيفة runBlockingTest. وهذه دالة توفّرها مكتبة اختبار الكوروتينات. وتأخذ هذه المجموعة جزءًا من الرمز، ثم تشغّل هذه المجموعة من الرموز في سياق كوروتين خاص يعمل بشكل متزامن وعلى الفور، ما يعني أنه سيتم تنفيذ الإجراءات بترتيب حاسم. ويؤدّي ذلك إلى عمل الكوروتينات مثل الكوروتين، لذا فهو مخصّص لاختبار الرموز.

استخدام runBlockingTest في صفوف الاختبار عند استدعاء دالة suspend. ستتعرّف على المزيد من المعلومات عن آلية عمل runBlockingTest وكيفية اختبار الكوروتينات في الدرس التطبيقي التالي حول الترميز في هذه السلسلة.

  1. أضِف @ExperimentalCoroutinesApi أعلى الصف. ويوضح ذلك أنك تعرف أنك تستخدم واجهة برمجة تطبيقات للإصدارات التجريبية من الكوروتين (runBlockingTest) في الصف الدراسي. وبدون ذلك، ستتلقى تحذيرًا.
  2. ارجع إلى DefaultTasksRepositoryTest، وأضِف runBlockingTest حتى يخضع للاختبار بالكامل كـ "block"

يبدو هذا الاختبار النهائي على أنه الرمز الوارد أدناه.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. نفِّذ اختبار getTasks_requestsAllTasksFromRemoteDataSource الجديد وتأكَّد من أنه يعمل وفقًا للخطأ.

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

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

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

في هذه المهمة، يمكنك تطبيق إدخال التبعية لعرض النماذج.

الخطوة الأولى: إنشاء واجهة "مهام Google"

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

كيف يبدو هذا الأمر عمليًا؟ انظر إلى TasksRemoteDataSource وTasksLocalDataSource وFakeDataSource، ولاحظوا أن جميعهم يشتركون في الواجهة نفسها: TasksDataSource. وهذا يسمح لك بأن تقول في طريقة إنشاء DefaultTasksRepository أنك ستأخذ TasksDataSource.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

وهي تتيح لنا تبديل جهازك في FakeDataSource.

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

  1. افتَح DefaultTasksRepository وانقر بزر الماوس الأيمن على اسم الصف. بعد ذلك، اختَر ReACTOR -> استخراج -> واجهة.

  1. اختَر استخراج ملفات منفصلة.

  1. في نافذة استخراج الواجهة، غيّر اسم الواجهة إلى TasksRepository.
  2. في القسم واجهة أعضاء النموذج، تحقَّق من جميع الأعضاء باستثناء العضوَين المصاحبَين والطُرق الخاصة الخاصة.


  1. انقر على إعادة بناء. يجب أن تظهر واجهة TasksRepository الجديدة في حزمة البيانات/المصدر.

ويطبّق DefaultTasksRepository الآن TasksRepository.

  1. شغِّل تطبيقك (وليس الاختبارات) للتأكد من أن كل شيء لا يزال صالحًا للعمل.

الخطوة الثانية: إنشاء FakeTestRepository

الآن بعد إنشاء الواجهة، يمكنك إنشاء اختبار DefaultTaskRepository مرتين.

  1. في المجموعة المصدر test، في data/source، أنشئ ملف Kotlin والفئة FakeTestRepository.kt وتمتد من واجهة TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

سيُطلب منك تنفيذ طرق الواجهة.

  1. مرِّر مؤشر الماوس فوق الخطأ حتى تظهر لك قائمة الاقتراحات، ثم انقر على تنفيذ الأعضاء.
  1. اختَر جميع الطرق واضغط على حسنًا.

الخطوة الثالثة. تطبيق طرق FakeTestRepository

لديك الآن صف واحد (FakeTestRepository) مع طُرق تنفيذ&& التنفيذ؛ على غرار آلية تنفيذ FakeDataSource، سيتم دعم FakeTestRepository ببنية البيانات بدلاً من التعامل مع توسط معقد بين مصادر البيانات المحلية والبعيدة.

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

  1. في FakeTestRepository، أضِف كلاً من المتغيّر LinkedHashMap الذي يمثّل القائمة الحالية للمهام وMutableLiveData للمهام القابلة للملاحظة.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

نفِّذ الطرق التالية:

  1. getTasks: يجب استخدام tasksServiceData مع هذه الطريقة وتحويلها إلى قائمة باستخدام tasksServiceData.values.toList() ثم عرضها كنتيجة Success.
  2. refreshTasks: يتم تعديل قيمة observableTasks لتكون ما تعرضه الدالة getTasks().
  3. observeTasks: إنشاء كوروتين باستخدام runBlocking وتشغيل refreshTasks، ثم عرض observableTasks.

وفي ما يلي رمز هذه الطرق.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

الخطوة الرابعة: إضافة طريقة للاختبار إلى addTasks

عند إجراء الاختبار، من الأفضل أن يكون لديك Tasks في مستودعك. يمكنك الاتصال بـ saveTask عدة مرات، ولكن لتسهيل ذلك، يمكنك إضافة طريقة مساعدة مخصّصة للاختبارات التي تتيح لك إضافة مهام.

  1. أضِف طريقة addTasks، التي تستغرق vararg من المهام، وتضيف كل واحدة منها إلى HashMap، ثم تعيد تحميل المهام.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

لديك في الوقت الحالي مستودع مزيف للاختبار باستخدام بعض الطرق الرئيسية التي تم تنفيذها. بعد ذلك، استخدم هذا في اختباراتك!

في هذه المهمة، يمكنك استخدام صف دراسي مزيّف داخل ViewModel. استخدِم إدخال تبعية المُنشئ، لتأخذ مصدرَي البيانات عن طريق إدخال التبعية للمُنشئ عن طريق إضافة متغيّر TasksRepository إلى طريقة الإنشاء TasksViewModel's.

تختلف هذه العملية قليلاً مع نماذج العرض لأنك لا تنشئها مباشرةً. على سبيل المثال:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


كما هو الحال في الرمز أعلاه، أنت تستخدم تفويض الموقع في viewModel's الذي ينشئ نموذج الملف الشخصي. لتغيير كيفية إنشاء نموذج العرض، ستحتاج إلى إضافة ViewModelProvider.Factory واستخدامه. إذا لم تكن على دراية بـ ViewModelProvider.Factory، يمكنك معرفة المزيد من المعلومات هنا.

الخطوة الأولى: إنشاء وعرض_نموذج في "مهام مهام العرض"

ستبدأ بتعديل الصفوف والاختبار المرتبط بشاشة Tasks.

  1. فتح TasksViewModel:
  2. تغيير طريقة إنشاء TasksViewModel بحيث يمكن استخدامها في TasksRepository بدلاً من إنشائها داخل الصف.

TasksViewmodel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

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

  1. في أسفل الملف TasksViewModel، خارج الصف، أضِف TasksViewModelFactory الذي يأخذ العمود TasksRepository عادي.

TasksViewmodel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


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

  1. يجب تحديث TasksFragment لاستخدام المصنع.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. يمكنك تشغيل رمز تطبيقك والتأكد من أن كل شيء لا يزال على ما يرام.

الخطوة الثانية: استخدام FakeTestRepository داخل "مهام"

وبدلاً من استخدام المستودع الفعلي في اختبارات نماذج العرض، يمكنك استخدام المستودع المزيف.

  1. افتح TasksViewModelTest.
  2. أضف خاصية FakeTestRepository في TasksViewModelTest.

TaskViewmodelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. عدِّل طريقة setupViewModel لإنشاء FakeTestRepository باستخدام ثلاث مهام، ثم أنشئ tasksViewModel باستخدام هذا المستودع.

TasksViewmodelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. يمكنك أيضًا إزالة التعليق التوضيحي @RunWith(AndroidJUnit4::class) لأنه لم تعد تستخدم رمز اختبار AndroidX ApplicationProvider.getApplicationContext.
  2. احرص على إجراء الاختبارات وتأكّد من أنّها ما زالت تعمل.

من خلال استخدام حقن الاعتمادات الإنشائية، تكون قد أزلت DefaultTasksRepository بصفتها تبعية واستبِدلتها بـ FakeTestRepository في الاختبارات.

الخطوة الثالثة. تعديل أيضًا تجزئة مهام ونموذج العرض

أدخِل التغييرات نفسها على TaskDetailFragment وTaskDetailViewModel. وسيؤدي هذا الإجراء إلى إعداد الرمز عند كتابة اختبارات TaskDetail التالية.

  1. فتح TaskDetailViewModel:
  2. حدِّث طريقة الإنشاء:

TaskDetailsViewmodel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. أضِف TaskDetailViewModelFactory في أسفل الملف TaskDetailViewModel خارج الصف.

TaskDetailsViewmodel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. يجب تحديث TasksFragment لاستخدام المصنع.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. شغّل الرمز وتأكد من أن كل شيء يعمل بشكل صحيح.

يمكنك الآن استخدام FakeTestRepository بدلاً من المستودع الفعلي في TasksFragment وTasksDetailFragment.

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

  • نمط ServiceLocator
  • مكتبات الإسبريسو والموكيتو

تختبر اختبارات الدمج تفاعل العديد من الصفوف الدراسية للتأكُّد من أنها تعمل بالشكل المتوقع عند استخدامها معًا. يمكن إجراء هذه الاختبارات إما محليًا (test مجموعة مصادر) أو كاختبارات قياس قياس (androidTest مجموعة مصادر).

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

الخطوة الأولى: إضافة تبعيات Gradle

  1. أضِف تبعيات الدرجات التالية.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

وتتضمن هذه الارتباطات:

  • junit:junit: JUnit، وهو ضروري لكتابة عبارات الاختبار الأساسية.
  • androidx.test:core: مكتبة اختبار AndroidX الأساسية
  • kotlinx-coroutines-test: مكتبة اختبار الكوروتين
  • androidx.fragment:fragment-testing: مكتبة اختبار AndroidX لإنشاء أجزاء في الاختبارات وتغيير حالتها.

بما أنّك ستستخدم هذه المكتبات في مجموعة مصادر androidTest، استخدِم androidTestImplementation لإضافتها إلى تبعياتها.

الخطوة الثانية: إنشاء دورة تدريبية في TaskDetailsFragmentTest

يعرض TaskDetailFragment معلومات حول مهمة واحدة.

ستبدأ بكتابة اختبار اختبار لـ TaskDetailFragment بما أنه يحتوي على وظائف أساسية إلى حد ما مقارنةً بالأجزاء الأخرى.

  1. فتح taskdetail.TaskDetailFragment:
  2. أنشئ اختبارًا للنطاق TaskDetailFragment، على النحو الذي فعلته من قبل. اقبل الاختيارات التلقائية وضعها في مجموعة مصادر androidTest (وليس في مجموعة مصادر test).

  1. أضِف التعليقات التوضيحية التالية إلى الصف TaskDetailFragmentTest.

TaskDetailsFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

والغرض من هذا التعليق التوضيحي هو:

  • @MediumTest: للإشارة إلى الاختبار باعتباره "&&مقاسة وقت التشغيل المتوسط" (مقارنةً باختبارات الوحدة @SmallTest واختبارات @LargeTest الكبيرة). ويساعدك ذلك على تجميع الاختبار واختياره.
  • @RunWith(AndroidJUnit4::class): يُستخدم هذا الإعداد في أي صف باستخدام اختبار AndroidX.

الخطوة الثالثة. تشغيل جزء من اختبار

في هذه المهمة، ستطلق TaskDetailFragment باستخدام مكتبة اختبارات AndroidX. FragmentScenario هي فئة من اختبار AndroidX يتم تضمينها حول جزء من الاختبار وتمنحك إمكانية التحكُّم المباشر في الجزء المتغيّر من الاختبار. لكتابة اختبارات للأجزاء، عليك إنشاء FragmentScenario للجزء الذي تختبره (TaskDetailFragment).

  1. نسخ هذا الاختبار إلى TaskDetailFragmentTest

TaskDetailsFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

الرمز التالي:

  • إنشاء مهمة.
  • تنشئ Bundle، والتي تمثل وسيطات الأجزاء للمهمة التي يتم تمريرها إلى الجزء.
  • تنشئ الدالة launchFragmentInContainer FragmentScenario، باستخدام هذه الحزمة ومظهر.

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

  1. هذا اختبار يتم قياسه، لذا تأكّد من ظهور المحاكي أو جهازك.
  2. إجراء الاختبار.

يجب إجراء بعض الأمور.

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

وأخيرًا، انظر بإمعان ولاحظ أن الجزء يقول "&data;No data" حيث إنه لا يحمّل بيانات المهمة بنجاح.

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

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

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

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

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

بما أنّه لا يمكنك إنشاء الجزء، لا يمكنك استخدام إدخال تبعية الدالة الإنشائية لتبديل اختبار المستودع مرتين (FakeTestRepository) إلى الجزء. بدلاً من ذلك، استخدِم النمط محدِّد خدمة. يُعدّ نمط "محدِّد الخدمة" بديلاً عن حقن التبعية. ويشمل ذلك إنشاء فئة مفردة تُسمى "&&حاصل تحديد موقع الخدمة" والغرض منه هو توفير تبعيات لكلٍّ من الرمز العادي والاختبار. في رمز التطبيق العادي (مجموعة المصدر main)، تكون كل الاعتماديات هذه هي ارتباطات تطبيقات عادية. بالنسبة إلى الاختبارات، يمكنك تعديل محدِّد مواقع الخدمات لتقديم إصدارات مزدوجة من التبعيات.

عدم استخدام محدِّد مواقع الخدمات


استخدام محدِّد مواقع الخدمات

ويجب تنفيذ ما يلي في هذا التطبيق التطبيقي حول الترميز:

  1. أنشئ صفًا لمحدد الخدمة الذي يمكنه إنشاء مستودع وتخزينه. وتُنشئ هذه الأداة تلقائيًا مستودعًا &&عرضًا عاديًا.
  2. أعد إنشاء الرمز بحيث يمكنك استخدام "محدِّد الخدمة" عند الحاجة إلى مستودع.
  3. في صف الاختبار، اتّبع طريقة على "محدِّد الخدمة" تعمل على استبدال "normal;quot; المستودع الاختباري المزدوج.

الخطوة الأولى: إنشاء معرّف الخدمة

لنجعل صف ServiceLocator. وسيكون متوفرًا في المصدر الرئيسي الذي تم إعداده مع بقية رمز التطبيق لأنه قيد الاستخدام بواسطة رمز التطبيق الرئيسي.

ملاحظة: ServiceLocator عبارة عن فردي، لذا استخدِم الكلمة الرئيسية object بلغة Kotlin للصف.

  1. أنشئ الملف ServiceLocator.kt في المستوى العلوي لمجموعة المصدر الرئيسية.
  2. وعليك تحديد object باسم ServiceLocator.
  3. إنشاء متغيّرَي مثيلات database وrepository وضبط كلٍّ منهما على null.
  4. يمكنك إضافة تعليقات توضيحية إلى المستودع باستخدام @Volatile لأنه يمكن استخدامها في سلاسل محادثات متعددة (@Volatile شرح مفصّل هنا)

ويجب أن يظهر الرمز كما هو موضّح أدناه.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

إنّ الطريقة الوحيدة التي يجب أن يتّبعها ServiceLocator هي معرفة كيفية عرض TasksRepository. ستعرض هذه السمة DefaultTasksRepository الموجودة من قبل أو تُعيد DefaultTasksRepository وتعرضه، إذا لزم الأمر.

حدد الوظائف التالية:

  1. provideTasksRepository: تقدِّم إمّا مستودعًا حاليًا أو تنشئ مستودعًا جديدًا. يجب أن تكون هذه الطريقة هي synchronized على this لتجنُّب إنشاء مثيلين للمستودع، عن طريق الخطأ، في حال توفّر سلاسل محادثات متعددة.
  2. createTasksRepository: رمز لإنشاء مستودع جديد سيتم الاتصال بالرقم createTaskLocalDataSource وإنشاء TasksRemoteDataSource جديد.
  3. createTaskLocalDataSource: رمز لإنشاء مصدر بيانات محلي جديد. سيتم الاتصال بالرقم createDataBase.
  4. createDataBase: رمز إنشاء قاعدة بيانات جديدة.

يمكنك الاطّلاع على الرمز الذي تم إكماله أدناه.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

الخطوة الثانية: استخدام ServiceLocator في التطبيق

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

من المهم أن تنشئ نسخة واحدة فقط من فئة المستودع. وللتأكّد من ذلك، ستستخدم معلِّم الخدمة في صف التطبيقات.

  1. في أعلى مستوى من العرض الهرمي للحزمة، افتح TodoApplication وأنشئ val لمستودعك وخصِّص له مستودعًا يتم الحصول عليه باستخدام ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

الآن وبعد إنشاء مستودع في التطبيق، يمكنك إزالة طريقة getRepository القديمة في DefaultTasksRepository.

  1. افتح DefaultTasksRepository واحذف الكائن المصاحب.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

في كل مكان كنت تستخدم فيه getRepository، يمكنك استخدام التطبيق taskRepository بدلاً من ذلك. وهذا يضمن أنه بدلاً من إنشاء المستودع مباشرةً، ستحصل على أي مستودع مقدّم من ServiceLocator.

  1. افتح TaskDetailFragement وابحث عن المكالمة التي تم إرسالها إلى getRepository في أعلى الصف.
  2. استبدل هذه المكالمة بمكالمة تحصل على المستودع من TodoApplication.

TaskDetailsFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. يمكنك تنفيذ الإجراء نفسه مع TasksFragment.

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. بالنسبة إلى StatisticsViewModel وAddEditTaskViewModel، يُرجى تعديل الرمز الذي يكتسب المستودع لاستخدام المستودع من TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. شغِّل تطبيقك (وليس الاختبار)!

وبما أنّك قد أجريت عملية إعادة التركيب فقط، يجب تشغيل التطبيق نفسه بدون مشكلة.

الخطوة الثالثة. إنشاء FakeAndroidTestRepository

لديك FakeTestRepository حاليًا في مجموعة مصادر الاختبار. لا يمكنك مشاركة فئات الاختبار بين مجموعات مصادر test وandroidTest بشكل تلقائي. لذلك، عليك إنشاء صف FakeTestRepository مكرر في مجموعة المصدر androidTest، واستدعاءه FakeAndroidTestRepository.

  1. انقر بزر الماوس الأيمن على مجموعة مصادر androidTest وأنشئ حزمة data. انقر بزر الماوس الأيمن مرة أخرى وأنشئ حزمة مصدر.
  2. أنشئ صفًا جديدًا في حزمة المصدر هذه باسم FakeAndroidTestRepository.kt.
  3. انسخ الرمز التالي إلى هذا الصف.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

الخطوة الرابعة: تحضير مُحدِّد الخدمة للاختبارات

تَمَامْ، دَهْ وَقْتِ اسْتِخْدَامْ ServiceLocator لِبَدْلِ الْمُكَالْمَة فِي الْفَتْحَة. لإجراء ذلك، عليك إضافة رمز إلى رمز ServiceLocator.

  1. فتح ServiceLocator.kt:
  2. ضَع علامة على اسم tasksRepository باعتباره @VisibleForTesting. يُعد هذا التعليق التوضيحي طريقة للتعبير عن السبب وراء ضبط التعيين على "عام" بسبب الاختبار.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

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

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

  1. أضِف متغيّر مثيل اسمه lock مع القيمة Any.

ServiceLocator.kt

private val lock = Any()
  1. أضِف طريقة خاصة بالاختبار اسمها resetRepository، ما يؤدي إلى محو قاعدة البيانات وضبط المستودع وقاعدة البيانات على قيمة فارغة.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

الخطوة الخامسة. استخدام معرّف الخدمة

في هذه الخطوة، يمكنك استخدام ServiceLocator.

  1. فتح TaskDetailFragmentTest:
  2. حدِّد متغيّر lateinit TasksRepository.
  3. أضِف إعدادًا وطريقة لإزالة FakeAndroidTestRepository لإعداده قبل كل اختبار وتنظيفه بعد كل اختبار.

TaskDetailsFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. التفاف نص الدالة activeTaskDetails_DisplayedInUi() في runBlockingTest.
  2. عليك حفظ activeTask في المستودع قبل تشغيل الجزء.
repository.saveTask(activeTask)

يبدو الاختبار النهائي على النحو التالي:

TaskDetailsFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. يمكنك إضافة تعليقات توضيحية إلى الصف بأكمله باستخدام @ExperimentalCoroutinesApi.

عند الانتهاء، سيظهر الرمز على النحو التالي.

TaskDetailsFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. إجراء اختبار activeTaskDetails_DisplayedInUi()

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


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

يساعدك الإسبريسو على:

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

الخطوة الأولى: ملاحظة اعتماد الاعتماد على Gradle

سيكون لديك تبعية الإسبرسو الرئيسية بالفعل لأنه يتم تضمينها في مشاريع Android بشكل تلقائي.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core: يتم تضمين هذه التبعية الأساسية لـ Espresso تلقائيًا عند إنشاء مشروع جديد على Android. وهو يحتوي على رمز الاختبار الأساسي لمعظم المشاهدات والإجراءات عليها.

الخطوة الثانية: إيقاف الصور المتحركة

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

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

  1. على جهاز الاختبار، انتقِل إلى الإعدادات &gt؛ خيارات المطوّرين.
  2. أوقِف هذه الإعدادات الثلاثة: مقياس الرسوم المتحركة للنافذة ومقياس الصور المتحركة للنقل ومقياس مدة الصور المتحركة.

الخطوة الثالثة. انظر إلى اختبار الإسبريسو.

قبل كتابة اختبار الإسبريسو، ألقِ نظرة على بعض رموز الإسبريسو.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

ويتمثّل هذا البيان في البحث عن طريقة عرض مربّع الاختيار التي تحمل رقم التعريف task_detail_complete_checkbox والنقر عليه ثم تأكيده.

تتألف غالبية كشوف الإسبريسو من أربعة أجزاء:

1. طريقة الإسبريسو الثابتة

onView

onView هي مثال على طريقة استخدام إسبريسو ثابتة لبدء بيان الإسبرسو. وتُعد onView من أكثر الخيارات شيوعًا، ولكن هناك خيارات أخرى، مثل onData.

2. ViewViewer

withId(R.id.task_detail_title_text)

withId هي مثال على ViewMatcher تحصل على مشاهدة حسب رقم تعريفها. هناك أدوات مطابقة أخرى للملف الشخصي يمكنك البحث عنها في المستندات.

3- ViewAction

perform(click())

طريقة perform التي تستخدم ViewAction. ViewAction هو إجراء يمكن تنفيذه في الملف الشخصي، على سبيل المثال، النقر على الملف الشخصي.

4. عرض التأكيد

check(matches(isChecked()))

check والتي تستغرق ViewAssertion. تتحقّق ViewAssertion من صحة الصورة أو تؤكّدها. يشكّل تأكيد matches الأكثر استخدامًا ViewAssertion. لإنهاء عملية التأكيد، استخدِم ViewMatcher آخر، في هذه الحالة isChecked.

يُرجى العلم بأنك لا تتصل دائمًا بكل من perform وcheck في بيان إسبرسو. يمكنك الحصول على كشوفات للتأكيد فقط باستخدام check أو ViewAction فقط باستخدام perform.

  1. فتح TaskDetailFragmentTest.kt:
  2. عدِّل اختبار activeTaskDetails_DisplayedInUi.

TaskDetailsFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

فيما يلي كشوفات الاستيراد، إذا لزم الأمر:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. كل الميزات التي تلي التعليق على // THEN تستخدم الإسبريسو. تحقّق من بنية الاختبار واستخدام withId وتحقّق من كيفية ظهور صفحة التفاصيل.
  2. أجرِ الاختبار وأكِّد نجاحه.

الخطوة الرابعة: كتابة اختبار الإسبريسو الخاص بك اختياريًا

والآن اكتب اختبارًا بنفسك.

  1. أنشِئ اختبارًا جديدًا باسم completedTaskDetails_DisplayedInUi وانسخ رمز الهيكل العظمي هذا.

TaskDetailsFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. بعد الاطّلاع على الاختبار السابق، يمكنك إكمال هذا الاختبار.
  2. نفِّذ وأكِّد نجاح الاختبارات.

يجب أن يظهر completedTaskDetails_DisplayedInUi الانتهاء على النحو التالي لهذا الرمز.

TaskDetailsFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

في هذه الخطوة الأخيرة، ستتعرّف على كيفية اختبار مكوّن التنقّل، باستخدام نوع مختلف من الاختبارات ثنائية يُطلق عليها اسم وهمية ومكتبة الاختبار Mockito.

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

فكِّر في كيفية التنقل. تخيَّل أنك ضغطت على إحدى المهام في TasksFragment للانتقال إلى شاشة تفاصيل المهمة.

رمز هنا في TasksFragment ينتقل إلى شاشة تفاصيل المهمة عند الضغط عليها.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


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

ما يمكنك تأكيده هو أنّه تم استدعاء طريقة navigate باستخدام معلّمة الإجراء الصحيحة. وهذا هو بالضبط ما يقوم به اختبار اختباري مرتين، وهو التحقق مما إذا تم استدعاء طرق محددة أم لا.

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

ستستخدم Mockito لإنشاء وهمية NavigationController يمكنها تأكيد أن طريقة التنقل تم استدعاؤها بشكل صحيح.

الخطوة الأولى: إضافة تبعيات Gradle

  1. أضِف تبعيات الدرجات.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core—هذه هي اعتمادية موكيتو.
  • dexmaker-mockito: هذه المكتبة مطلوبة لاستخدام Mockito في مشروع متوافق مع Android. تحتاج Mockito إلى إنشاء صفوف في وقت التشغيل. على نظام التشغيل Android، يتم إجراء ذلك باستخدام رمز dex بايت، وبالتالي تسمح هذه المكتبة لتطبيق Mockito بإنشاء العناصر أثناء وقت التشغيل على Android.
  • androidx.test.espresso:espresso-contrib: تتألف هذه المكتبة من مساهمات خارجية (باسمها) تحتوي على رمز اختبار لمشاهدات أكثر تقدمًا، مثل DatePicker وRecyclerView. وتتضمّن أيضًا عمليات تحقّق من تسهيل الاستخدام وفئة باسم CountingIdlingResource يتم تناولها لاحقًا.

الخطوة الثانية: إنشاء مهامFFmentmentTest

  1. فتح TasksFragment
  2. انقر بزر الماوس الأيمن على اسم الفئة في TasksFragment واختَر إنشاء ثم اختبار. أنشئ اختبارًا في مجموعة مصادر androidTest.
  3. يمكنك نسخ هذا الرمز إلى TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

يبدو هذا الرمز مشابهًا لرمز TaskDetailFragmentTest الذي كتبته. تُعِدّ هذه الميزة FakeAndroidTestRepository وينزع عنه. يمكنك إضافة اختبار تنقّل للتأكّد من أنه عند النقر على مهمة في قائمة المهام، يتم توجيهك إلى مربع TaskDetailFragment الصحيح.

  1. أضِف الاختبار clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. استخدام دالة mock Mockito' لإنشاء نموذج وهمي

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

للمحاكاة على Mockito، انتقِل إلى الصف الذي تريد محاكاةه.

بعد ذلك، يجب ربط NavController بالأجزاء. يسمح لك onFragment باستدعاء الطرق في الجزء نفسه.

  1. اجعل النموذج الجديد يحاكي الجزء NavController.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. أضف الرمز للنقر على العنصر في RecyclerView الذي يحتوي على النص "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions هي جزء من مكتبة espresso-contrib وتتيح لك تنفيذ إجراءات الإسبريسو على RecyclerView.

  1. تحقَّق من أنه تم استدعاء الدالة navigate مع الوسيطة الصحيحة.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

طريقة Mockito's هي ما يجعل هذه العملية وهمية، وأنت قادر على تأكيد الرمز navController الذي تم محاكاةه باستخدام طريقة معيّنة (navigate) باستخدام إحدى المعلَمات (actionTasksFragmentToTaskDetailFragment باستخدام معرّف "&معرّف_معرّف_التعريف_1&").

ويبدو الاختبار الكامل على النحو التالي:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. إجراء الاختبار

وباختصار، لاختبار التنقل، يمكنك:

  1. استخدم Mockito لإنشاء نموذج وهمي NavController.
  2. أرفِق ذلك بالوهم NavController إلى الجزء.
  3. تحقّق من أنه تم استدعاء التنقّل باستخدام الإجراء والمعلّمات الصحيحة.

الخطوة الثالثة. اختياري، اكتب clickAddTaskButton_NavigationToAddEditFragment

ولمعرفة ما إذا كان بإمكانك كتابة اختبار تنقّل بنفسك، جرِّب هذه المهمة.

  1. اكتب الاختبار clickAddTaskButton_navigateToAddEditFragment الذي يتحقّق من أنك إذا نقرت على زر الإجراء + FAB، سيتم الانتقال إلى AddEditTaskFragment.

الإجابة أدناه.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

انقر هنا لمعرفة الفرق بين الرمز الذي بدأته والرمز النهائي.

لتنزيل رمز الدرس التطبيقي للترميز، يمكنك استخدام الأمر git أدناه:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


بدلاً من ذلك، يمكنك تنزيل المستودع كملف Zip وفك ضغطه وفتحه في"استوديو Android".

تنزيل ملف Zip

يتناول هذا الدرس التطبيقي كيفية إعداد إدخال التبعية اليدوية ومُحدِّد مواقع الخدمات وكيفية استخدام العناصر المزيفة والاصطناعية في تطبيقات Android Kotlin. وعلى وجه الخصوص:

  • ما تريد اختباره واستراتيجية اختبارك تحدّد أنواع الاختبارات التي ستنفّذها لتطبيقك. يتم التركيز على اختبارات الوحدة بشكلٍ سريع. تعمل اختبارات الدمج على التحقق من التفاعل بين أجزاء برنامجك. تعمل الاختبارات المتقدمة على التحقق من الميزات وأعلى دقة، وغالبًا ما يتم استخدام هذه الأدوات وقد يستغرق تشغيلها وقتًا أطول.
  • تؤثر بنية تطبيقك في مدى صعوبة اختباره.
  • TDD أو التطوير المستند إلى الاختبار هو استراتيجية تكتب فيها الاختبارات أولاً، ثم تنشئ الميزة لاجتياز الاختبارات.
  • لعزل أجزاء من تطبيقك لأغراض الاختبار، يمكنك استخدام أدوات الاختبار المزدوجة. الاختبار المزدوج هو إصدار من صف تم تصميمه خصيصًا للاختبار. على سبيل المثال، يمكنك تزييف الحصول على البيانات من قاعدة بيانات أو إنترنت.
  • استخدِم إدخال التبعية لاستبدال فئة فعلية بفئة اختبار، مثل مستودع أو طبقة شبكة.
  • استخدِم الاختبار المضمّن (androidTest) لتشغيل مكوّنات واجهة المستخدم.
  • عندما يتعذّر عليك استخدام إدخال تبعية لدالة الإنشاء، على سبيل المثال لإطلاق جزء، يمكنك في كثير من الأحيان استخدام محدِّد مواقع الخدمات. يُعدّ نمط محدِّد مواقع الخدمات بديلاً عن إدخال التبعية. ويشمل ذلك إنشاء فئة مفردة تُسمى "&&حاصل تحديد موقع الخدمة" والغرض منه هو توفير تبعيات لكلٍّ من الرمز العادي والاختبار.

دورة Udacity:

مستندات مطوّر برامج Android:

فيديوهات:

غير ذلك:

للحصول على روابط إلى دروس تطبيقية أخرى حول الترميز في هذه الدورة التدريبية، اطّلِع على الصفحة المقصودة للإصدارات المتقدّمة من Android في لغة ترميز Kotlin.