مقدمة حول Test Doubles وDependency Injection

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

مقدمة

يتناول هذا الدرس التطبيقي الثاني حول الاختبار كل ما يتعلّق ببدائل الاختبار: متى يتم استخدامها في Android، وكيفية تنفيذها باستخدام ميزة "إدخال التبعية" ونمط Service Locator والمكتبات. من خلال ذلك، ستتعرّف على كيفية كتابة:

  • اختبارات الوحدة الخاصة بالمستودع
  • اختبارات الدمج الخاصة بالتقسيمات وViewModel
  • اختبارات التنقّل بين الأجزاء

ما يجب معرفته

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

  • لغة البرمجة Kotlin
  • مفاهيم الاختبار التي تم تناولها في الدرس العملي الأول: كتابة اختبارات الوحدات وتشغيلها على Android باستخدام JUnit وHamcrest وAndroidX test وRobolectric، بالإضافة إلى اختبار LiveData
  • مكتبات Android Jetpack الأساسية التالية: ViewModel وLiveData وNavigation Component
  • بنية التطبيق، باتّباع النمط الوارد في دليل بنية التطبيق والدروس التطبيقية حول أساسيات Android
  • أساسيات الروتينات المشتركة على Android

أهداف الدورة التعليمية

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

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

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

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

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

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

تنزيل الرمز

للبدء، نزِّل الرمز باتّباع الخطوات التالية:

تنزيل ملف Zip

بدلاً من ذلك، يمكنك استنساخ مستودع Github للرمز:

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

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

الخطوة 1: تشغيل تطبيق العيّنة

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

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

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

يستند تطبيق TO-DO إلى نموذج الاختبار والتصميم الشائع Architecture Blueprints (باستخدام إصدار التصميم التفاعلي من النموذج). يتبع التطبيق البنية الواردة في دليل بنية التطبيق. يستخدم هذا التطبيق ViewModels مع Fragments ومستودعًا وRoom. إذا كنت على دراية بأي من الأمثلة أدناه، فإنّ هذا التطبيق يتضمّن بنية مشابهة:

من المهم أن تفهم البنية العامة للتطبيق أكثر من أن يكون لديك فهم عميق للمنطق في أي طبقة.

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

فئات الأدوات المساعدة: فئات مشترَكة تُستخدَم في أجزاء مختلفة من التطبيق، مثل تخطيط التحديث بالسحب المستخدَم على شاشات متعددة.

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

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

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

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

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

التنقّل

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

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

يتناول هذا القسم بعض أفضل الممارسات المتعلّقة بالاختبار بشكل عام، كما تنطبق على نظام التشغيل Android.

هرم الاختبار

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

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

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

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

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

البنية والاختبار

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

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



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

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

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

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

قد يكون اختبار الرمز البرمجي في فئة أو فئات معيّنة أمرًا صعبًا. لنلقِ نظرةً على المثال التالي. افتح فئة 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)، ما يتطلّب قدرًا أكبر من الرمز مقارنةً بمجرد رمز المستودع.

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

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

Test Doubles

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

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

زائف

عنصر بديل للاختبار يتضمّن تنفيذًا "يعمل" للفئة، ولكن يتم تنفيذه بطريقة تجعله مناسبًا للاختبارات وغير مناسب للإنتاج.

Mock

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

Stub

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

عنصر نائب

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

Spy

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

لمزيد من المعلومات حول عناصر الاختبار البديلة، يمكنك الاطّلاع على Testing on the Toilet: Know Your Test Doubles.

أكثر أنواع بدائل الاختبار شيوعًا في Android هما النماذج الوهمية وعمليات المحاكاة.

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

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

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

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

  1. أنشئ حزمة بيانات تتضمّن حزمة مصدر.
  2. أنشئ صفًا جديدًا باسم FakeDataSource في حزمة data/source.

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

لكي تتمكّن من استخدام فئة الاختبار الجديدة 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 Studio رسالة خطأ تفيد بأنّك لم تنفّذ الطرق المطلوبة لـ 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 }


هذه هي قائمة المهام التي "تتظاهر" بأنّها استجابة من قاعدة بيانات أو خادم. في الوقت الحالي، الهدف هو اختبار طريقة المستودع getTasks. يؤدي ذلك إلى استدعاء طرق 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()

حان الوقت لكتابة اختبار 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: إضافة runBlockingTest

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

  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 لكي يتم إدخال الاختبار بأكمله كـ "مجموعة" من الرموز البرمجية

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

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 الجديد وتأكَّد من أنّه يعمل وأنّ الخطأ قد تم إصلاحه.

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

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

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

في هذه المهمة، ستطبِّق ميزة "إدخال التبعية" على نماذج العرض.

الخطوة 1: إنشاء واجهة TasksRepository

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

كيف يبدو ذلك عمليًا؟ انظر إلى 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 وانقر بزر الماوس الأيمن على اسم الصف. بعد ذلك، اختَر إعادة تصميم -> استخراج -> الواجهة.

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

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


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

وتنفّذ DefaultTasksRepository الآن TasksRepository.

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

الخطوة 2: Create FakeTestRepository

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

  1. في مجموعة مصادر الاختبار، أنشئ ملف Kotlin وفئة FakeTestRepository.kt في data/source، ثم وسِّع نطاقها من واجهة TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

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

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

الخطوة 3: تنفيذ طرق 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

}

الخطوة 4: إضافة طريقة للاختبار إلى 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.

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

class TasksFragment : Fragment() {

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

}


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

الخطوة 1: إنشاء ViewModelFactory واستخدامه في TasksViewModel

ابدأ بتعديل الصفوف والاختبارات المرتبطة بالشاشة 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. نفِّذ رمز التطبيق وتأكَّد من أنّ كل شيء لا يزال يعمل.

الخطوة 2: استخدام FakeTestRepository داخل TasksViewModelTest

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

  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. بما أنّك لم تعُد تستخدم رمز AndroidX Test ApplicationProvider.getApplicationContext، يمكنك أيضًا إزالة التعليق التوضيحي @RunWith(AndroidJUnit4::class).
  2. نفِّذ اختباراتك وتأكَّد من أنّها لا تزال تعمل.

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

الخطوة 3: تعديل الفئة TaskDetail Fragment وViewModel أيضًا

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

  1. افتح TaskDetailViewModel.
  2. عدِّل الدالة الإنشائية:

TaskDetailViewModel.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. في أسفل ملف TaskDetailViewModel، خارج الفئة، أضِف TaskDetailViewModelFactory.

TaskDetailViewModel.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
  • مكتبتَي Espresso وMockito

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

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

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

  1. أضِف تبعيات Gradle التالية.

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 لإضافتها كعناصر اعتمادية.

الخطوة 2: إنشاء فئة TaskDetailFragmentTest

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

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

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

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

TaskDetailFragmentTest.kt

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

}

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

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

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

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

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

TaskDetailFragmentTest.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. نفِّذ الاختبار.

من المفترض أن يحدث ما يلي:

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

أخيرًا، انتبه جيدًا ولاحظ أنّ الجزء يعرض الرسالة "لا تتوفّر بيانات" لأنّه لم يتم تحميل بيانات المهمة بنجاح.

يجب أن يتم تحميل 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. سيتيح لك ذلك كتابة اختبارات التكامل الخاصة بالنماذج وعناصر Fragment وعرضها.

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

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

عدم استخدام Service Locator


استخدام أداة تحديد موقع الخدمة

بالنسبة إلى تطبيق هذا الدرس العملي، اتّبِع الخطوات التالية:

  1. أنشئ فئة Service Locator يمكنها إنشاء مستودع وتخزينه. ينشئ تلقائيًا مستودعًا "عاديًا".
  2. أعِد تصميم الرمز البرمجي بحيث تستخدم Service Locator عندما تحتاج إلى مستودع.
  3. في فئة الاختبار، استدعِ طريقة في Service Locator تستبدل المستودع "العادي" ببديل الاختبار.

الخطوة 1: إنشاء ServiceLocator

لننشئ فئة 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
    }
}

الخطوة 2: استخدام 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
            }
        }
    }
}

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

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

TaskDetailFragment.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. شغِّل تطبيقك (وليس الاختبار).

بما أنّك أعدت تصميم الرمز فقط، من المفترض أن يعمل التطبيق بالطريقة نفسها بدون أي مشاكل.

الخطوة 3: Create FakeAndroidTestRepository

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

  1. انقر بزرّ الماوس الأيمن على مجموعة المصادر androidTest وأنشئ حزمة بيانات. انقر بزر الماوس الأيمن مرة أخرى وأنشئ حزمة مصدر .
  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() }
    }
}

الخطوة 4: إعداد ServiceLocator للاختبارات

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

  1. افتح ServiceLocator.kt.
  2. ضَع علامة @VisibleForTesting على الدالة الضابطة للسمة tasksRepository. هذا التعليق التوضيحي هو طريقة للتعبير عن أنّ سبب إتاحة أداة الضبط للجميع هو الاختبار.

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
        }
    }

الخطوة 5: استخدام ServiceLocator

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

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

TaskDetailFragmentTest.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)

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

TaskDetailFragmentTest.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

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

TaskDetailFragmentTest.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 لإكمال اختبار الدمج الأول. لقد نظّمت الرمز البرمجي بطريقة تتيح لك إضافة اختبارات مع تأكيدات لواجهة المستخدم. لإجراء ذلك، عليك استخدام مكتبة اختبار Espresso.

تساعدك Espresso في ما يلي:

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

الخطوة 1: ملاحظة حول تبعية Gradle

سيكون لديك بالفعل التبعية الرئيسية لـ Espresso لأنّها مضمّنة في مشاريع 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 جديد. يحتوي على رمز الاختبار الأساسي لمعظم طرق العرض والإجراءات عليها.

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

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

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

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

الخطوة 3: إلقاء نظرة على اختبار Espresso

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

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

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

تتألف غالبية عبارات Espresso من أربعة أجزاء:

1. طريقة Static Espresso

onView

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

2. ViewMatcher

withId(R.id.task_detail_title_text)

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

3- ViewAction

perform(click())

الطريقة perform التي تأخذ ViewAction ViewAction هو إجراء يمكن تنفيذه على العرض، مثلاً النقر على العرض.

4. ViewAssertion

check(matches(isChecked()))

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

يُرجى العِلم أنّه ليس عليك دائمًا استدعاء كل من perform وcheck في عبارة Espresso. يمكنك تضمين عبارات تؤكّد على صحة معلومة باستخدام check أو عبارات تنفّذ إجراءً باستخدام ViewAction من خلال perform.

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

TaskDetailFragmentTest.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 Espresso. افحص بنية الاختبار واستخدام withId وتحقّق من صحة التأكيدات بشأن الشكل الذي يجب أن تبدو عليه صفحة التفاصيل.
  2. نفِّذ الاختبار وتأكَّد من اجتيازه.

الخطوة 4: اختياري، كتابة اختبار Espresso الخاص بك

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

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

TaskDetailFragmentTest.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 المكتمل على النحو التالي.

TaskDetailFragmentTest.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()))
    }

في هذه الخطوة الأخيرة، ستتعرّف على كيفية اختبار مكوّن Navigation باستخدام نوع مختلف من عناصر الاختبار البديلة يُعرف باسم "العنصر الوهمي"، بالإضافة إلى مكتبة الاختبار Mockito.

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

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

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

TasksFragment.kt

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


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

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

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

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

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

  1. أضِف تبعيات Gradle.

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

الخطوة 2: إنشاء TasksFragmentTest

  1. فتح "TasksFragment"
  2. انقر بزر الماوس الأيمن على اسم الفئة TasksFragment، ثم اختَر إنشاء (Generate) ثم اختبار (Test). أنشئ اختبارًا في مجموعة المصادر 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 ويتيح لك تنفيذ إجراءات Espresso على RecyclerView.

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

إنّ طريقة verify في Mockito هي ما يجعل هذا العنصر عنصرًا زائفًا، إذ يمكنك تأكيد أنّ العنصر الزائف navController قد استدعى طريقة معيّنة (navigate) مع مَعلمة (actionTasksFragmentToTaskDetailFragment تحمل رقم التعريف "id1").

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

@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. تأكَّد من أنّ الدالة navigate تم استدعاؤها باستخدام الإجراء والمعلَمات الصحيحة.

الخطوة 3: اختياري، اكتب clickAddTaskButton_navigateToAddEditFragment

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

  1. اكتب اختبار clickAddTaskButton_navigateToAddEditFragment يتحقّق من أنّه عند النقر على زر الإجراء العائم (+)، يتم الانتقال إلى 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 Studio.

تنزيل ملف Zip

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

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

دورة Udacity التدريبية:

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

فيديوهات:

غير ذلك:

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