هذا الدرس العملي حول الترميز هو جزء من دورة "تطبيقات متقدّمة متوافقة مع نظام 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، ويتبع بنية من دليل بنية التطبيق. من خلال التعرّف على كيفية اختبار هذا التطبيق، ستتمكّن من اختبار التطبيقات التي تستخدم المكتبات والبنية نفسها.
تنزيل الرمز
للبدء، نزِّل الرمز باتّباع الخطوات التالية:
بدلاً من ذلك، يمكنك استنساخ مستودع 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. إذا كنت على دراية بأي من الأمثلة أدناه، فإنّ هذا التطبيق يتضمّن بنية مشابهة:
- درس Room with a View التطبيقي حول الترميز
- البرامج التعليمية حول الترميز في دورة Android Kotlin Fundamentals التدريبية
- الدروس التطبيقية حول الترميز في التدريب المتقدّم على Android
- نموذج Sunflower لنظام التشغيل Android
- دورة "تطوير تطبيقات Android باستخدام لغة Kotlin" التدريبية على Udacity
من المهم أن تفهم البنية العامة للتطبيق أكثر من أن يكون لديك فهم عميق للمنطق في أي طبقة.
في ما يلي ملخّص للحِزم التي ستظهر لك:
الحزمة: | |
| شاشة إضافة مهمة أو تعديلها: رمز طبقة واجهة المستخدم لإضافة مهمة أو تعديلها |
| طبقة البيانات: تتعامل هذه الطبقة مع طبقة البيانات الخاصة بالمهام. ويحتوي على رمز قاعدة البيانات والشبكة والمستودع. |
| شاشة الإحصاءات: رمز طبقة واجهة المستخدِم لشاشة الإحصاءات |
| شاشة تفاصيل المهمة: رمز طبقة واجهة المستخدم لمهمة واحدة. |
| شاشة المهام: رمز طبقة واجهة المستخدم لقائمة جميع المهام |
| فئات الأدوات المساعدة: فئات مشترَكة تُستخدَم في أجزاء مختلفة من التطبيق، مثل تخطيط التحديث بالسحب المستخدَم على شاشات متعددة. |
طبقة البيانات (.data)
يتضمّن هذا التطبيق طبقة شبكة محاكاة في حزمة remote وطبقة قاعدة بيانات في حزمة local. لتبسيط الأمر، في هذا المشروع، يتم محاكاة طبقة الشبكة باستخدام HashMap
مع تأخير فقط، بدلاً من إجراء طلبات شبكة حقيقية.
تتولّى DefaultTasksRepository
التنسيق أو الوساطة بين طبقة الشبكة وطبقة قاعدة البيانات، وهي التي تعرض البيانات في طبقة واجهة المستخدم.
طبقة واجهة المستخدم ( .addedittask و.statistics و.taskdetail و.tasks)
تحتوي كل حزمة من حِزم طبقة واجهة المستخدم على جزء ونموذج عرض، بالإضافة إلى أي فئات أخرى مطلوبة لواجهة المستخدم (مثل أداة ربط لقائمة المهام). TaskActivity
هو النشاط الذي يحتوي على جميع الأجزاء.
التنقّل
يتم التحكّم في التنقّل في التطبيق من خلال مكوّن التنقّل. يتم تحديدها في الملف nav_graph.xml
. يتم بدء التنقّل في نماذج العرض باستخدام الفئة Event
، وتحدّد نماذج العرض أيضًا المَعلمات التي سيتم تمريرها. تراقب الأجزاء Event
وتنفّذ عملية التنقّل الفعلية بين الشاشات.
في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية اختبار المستودعات ونماذج العرض واللقطات باستخدام عناصر الاختبار البديلة وإدخال التبعية. قبل التعرّف على هذه الاختبارات، من المهم فهم الأسباب التي ستوجّهك في تحديد نوع الاختبارات وكيفية كتابتها.
يتناول هذا القسم بعض أفضل الممارسات المتعلّقة بالاختبار بشكل عام، كما تنطبق على نظام التشغيل Android.
هرم الاختبار
عند التفكير في استراتيجية اختبار، هناك ثلاثة جوانب اختبار ذات صلة:
- النطاق: ما هو الجزء من الرمز الذي يغطّيه الاختبار؟ يمكن إجراء الاختبارات على طريقة واحدة أو على التطبيق بأكمله أو على جزء منه.
- السرعة: ما هي سرعة تنفيذ الاختبار؟ يمكن أن تتراوح سرعات الاختبار بين أجزاء من الثانية وعدة دقائق.
- الدقة: ما مدى واقعية الاختبار؟ على سبيل المثال، إذا كان جزء من الرمز الذي تختبره يحتاج إلى تقديم طلب شبكة، هل يقدّم رمز الاختبار طلب الشبكة هذا فعلاً، أم أنّه يزوّر النتيجة؟ إذا كان الاختبار يتواصل مع الشبكة، يعني ذلك أنّه يتمتّع بدقة أعلى. في المقابل، قد يستغرق الاختبار وقتًا أطول لتنفيذه، أو قد يؤدي إلى حدوث أخطاء في حال تعذُّر الاتصال بالشبكة، أو قد يكون مكلفًا.
وهناك مفاضلات متأصّلة بين هذه الجوانب. على سبيل المثال، السرعة والدقة هما عاملان متضادّان، فكلما كان الاختبار أسرع، كانت الدقة أقلّ بشكل عام، والعكس صحيح. إحدى الطرق الشائعة لتقسيم الاختبارات المبرمَجة هي إلى الفئات الثلاث التالية:
- اختبارات الوحدات: هي اختبارات مركّزة للغاية يتم إجراؤها على فئة واحدة، وعادةً ما تكون طريقة واحدة في تلك الفئة. إذا تعذّر اجتياز اختبار الوحدة، يمكنك معرفة مكان المشكلة في الرمز البرمجي بالضبط. وتتسم هذه الاختبارات بدقتها المنخفضة لأنّ تطبيقك في العالم الحقيقي يتضمّن أكثر بكثير من تنفيذ طريقة أو فئة واحدة. وهي سريعة بما يكفي لتنفيذها في كل مرة تغيّر فيها الرمز. في معظم الأحيان، ستكون هذه الاختبارات محلية (في مجموعة المصادر
test
). مثال: اختبار طرق فردية في نماذج العرض والمستودعات - اختبارات الدمج: تختبر هذه الاختبارات تفاعل عدة فئات للتأكّد من أنّها تعمل على النحو المتوقّع عند استخدامها معًا. إحدى طرق تنظيم اختبارات الدمج هي أن تختبر ميزة واحدة، مثل القدرة على حفظ مهمة. تختبر هذه الاختبارات نطاقًا أكبر من الرموز البرمجية مقارنةً باختبارات الوحدات، ولكنها لا تزال محسّنة لتنفيذها بسرعة، بدلاً من الحصول على دقة كاملة. ويمكن تشغيلها إما محليًا أو كاختبارات أدوات، حسب الموقف. مثال: اختبار جميع وظائف جزء واحد وزوج من نماذج العرض
- اختبارات شاملة (E2e): تختبر هذه الاختبارات مجموعة من الميزات التي تعمل معًا. تختبر هذه الأنواع أجزاءً كبيرة من التطبيق، وتحاكي الاستخدام الفعلي بشكل كبير، وبالتالي تكون عادةً بطيئة. تتميّز هذه الاختبارات بأعلى دقة وتوضّح لك أنّ تطبيقك يعمل بشكل كامل. بشكل عام، ستكون هذه الاختبارات اختبارات مزوَّدة بأدوات (في مجموعة المصادر
androidTest
)
مثال: بدء تشغيل التطبيق بأكمله واختبار بعض الميزات معًا.
غالبًا ما يتم تمثيل النسبة المقترَحة لهذه الاختبارات بهرم، حيث تشكّل اختبارات الوحدات الغالبية العظمى من الاختبارات.
البنية والاختبار
إنّ قدرتك على اختبار تطبيقك على جميع المستويات المختلفة لهرم الاختبار مرتبطة بشكل أساسي بتصميم تطبيقك. على سبيل المثال، قد يضع تطبيق مصمَّم بشكل سيئ للغاية كل منطق التطبيق داخل طريقة واحدة. قد تتمكّن من كتابة اختبار شامل لهذه الحالة، لأنّ هذه الاختبارات تميل إلى اختبار أجزاء كبيرة من التطبيق، ولكن ماذا عن كتابة اختبارات الوحدات أو اختبارات الدمج؟ عندما تكون جميع التعليمات البرمجية في مكان واحد، يصعب اختبار التعليمات البرمجية المرتبطة بوحدة أو ميزة واحدة فقط.
ويتمثّل الأسلوب الأفضل في تقسيم منطق التطبيق إلى طرق وفئات متعدّدة، ما يسمح باختبار كل جزء على حدة. البنية هي طريقة لتقسيم الرمز البرمجي وتنظيمه، ما يتيح إجراء اختبارات الوحدة والاختبارات المتكاملة بسهولة أكبر. يتبع تطبيق قائمة المهام الذي ستختبره بنية معيّنة:
في هذا الدرس، ستتعرّف على كيفية اختبار أجزاء من البنية أعلاه، بشكل منفصل:
- أولاً، عليك إجراء اختبار وحدة المستودع.
- بعد ذلك، ستستخدم عنصرًا بديلًا في نموذج العرض، وهو أمر ضروري لإجراء اختبارات الوحدات واختبارات التكامل لنموذج العرض.
- بعد ذلك، ستتعلّم كيفية كتابة اختبارات تكامل للقِطع و"نماذج عرض".
- أخيرًا، ستتعلّم كيفية كتابة اختبارات الدمج التي تتضمّن مكوّن 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 | عنصر بديل للاختبار لا يتضمّن أي منطق ويعرض فقط ما يتم ضبطه لعرضه. يمكن برمجة |
عنصر نائب | عنصر بديل للاختبار يتم تمريره ولكن لا يتم استخدامه، مثلاً إذا كنت بحاجة إلى تقديمه كمعلَمة فقط. إذا كان لديك |
Spy | عنصر بديل للاختبار يتتبّع أيضًا بعض المعلومات الإضافية، مثلاً، إذا أجريت |
لمزيد من المعلومات حول عناصر الاختبار البديلة، يمكنك الاطّلاع على Testing on the Toilet: Know Your Test Doubles.
أكثر أنواع بدائل الاختبار شيوعًا في Android هما النماذج الوهمية وعمليات المحاكاة.
في هذه المهمة، ستنشئ FakeDataSource
عنصرًا بديلًا للاختبار من أجل إجراء اختبار وحدة DefaultTasksRepository
منفصل عن مصادر البيانات الفعلية.
الخطوة 1: إنشاء فئة FakeDataSource
في هذه الخطوة، ستنشئ فئة باسم FakeDataSouce
، والتي ستكون بديلاً للاختبار عن LocalDataSource
وRemoteDataSource
.
- في مجموعة مصادر الاختبار، انقر بزر الماوس الأيمن على جديد -> حزمة.
- أنشئ حزمة بيانات تتضمّن حزمة مصدر.
- أنشئ صفًا جديدًا باسم
FakeDataSource
في حزمة data/source.
الخطوة 2: تنفيذ واجهة TasksDataSource
لكي تتمكّن من استخدام فئة الاختبار الجديدة FakeDataSource
كبديل للاختبار، يجب أن تكون قادرة على استبدال مصادر البيانات الأخرى. مصادر البيانات هذه هي TasksLocalDataSource
وTasksRemoteDataSource
.
- لاحظ كيف أنّ كليهما ينفّذان واجهة
TasksDataSource
.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- اجعل
FakeDataSource
ينفّذTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
سيُظهر Android Studio رسالة خطأ تفيد بأنّك لم تنفّذ الطرق المطلوبة لـ TasksDataSource
.
- استخدِم قائمة الإصلاح السريع واختَر تنفيذ الأعضاء.
- اختَر جميع الطرق واضغط على حسنًا.
الخطوة 3: تنفيذ طريقة getTasks في FakeDataSource
FakeDataSource
هو نوع محدّد من بدائل الاختبار يُعرف باسم الزائف. الزائف هو بديل للاختبار يتضمّن تنفيذًا "يعمل" للفئة، ولكن يتم تنفيذه بطريقة تجعله مناسبًا للاختبارات وغير مناسب للاستخدام الفعلي. تعني عملية التنفيذ "تعمل" أنّ الفئة ستنتج مخرجات واقعية بالنظر إلى المدخلات.
على سبيل المثال، لن يرتبط مصدر البيانات الوهمي بالشبكة أو يحفظ أي بيانات في قاعدة بيانات، بل سيستخدم قائمة في الذاكرة فقط. سيعمل هذا الإعداد "كما هو متوقّع"، أي أنّ الطرق التي يتم من خلالها الحصول على المهام أو حفظها ستعرض النتائج المتوقّعة، ولكن لا يمكنك استخدام هذا التنفيذ في مرحلة الإنتاج، لأنّه لا يتم حفظه على الخادم أو في قاعدة بيانات.
FakeDataSource
- تتيح لك اختبار الرمز في
DefaultTasksRepository
بدون الحاجة إلى الاعتماد على قاعدة بيانات أو شبكة فعلية. - توفّر عملية تنفيذ "واقعية بما يكفي" للاختبارات.
- غيِّر الدالة الإنشائية
FakeDataSource
لإنشاءvar
باسمtasks
يكونMutableList<Task>?
بقيمة تلقائية لقائمة قابلة للتغيير فارغة.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
هذه هي قائمة المهام التي "تتظاهر" بأنّها استجابة من قاعدة بيانات أو خادم. في الوقت الحالي، الهدف هو اختبار طريقة المستودع getTasks
. يؤدي ذلك إلى استدعاء طرق getTasks
وdeleteAllTasks
وsaveTask
لمصدر البيانات .
اكتب نسخة مزيفة من هذه الطرق:
- اكتب
getTasks
: إذا لم يكنtasks
null
، أرجِع النتيجةSuccess
. إذا كانت قيمةtasks
هيnull
، اعرض النتيجةError
. - اكتب
deleteAllTasks
: لمحو قائمة المهام القابلة للتعديل. - اكتب
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
- غيِّر الدالة الإنشائية
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 }
- بما أنّك مرّرت العناصر التابعة، أزِل الطريقة
init
. لم يعُد عليك إنشاء العناصر التابعة. - احذف أيضًا متغيرات المثيل القديمة. يمكنك تحديدها في الدالة الإنشائية:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- أخيرًا، عدِّل طريقة
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
.
- انقر بزر الماوس الأيمن على اسم الفئة
DefaultTasksRepository
واختَر إنشاء، ثم اختبار. - اتّبِع التعليمات لإنشاء
DefaultTasksRepositoryTest
في مجموعة المصادر الاختبارية. - في أعلى فئة
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 }
- أنشئ ثلاثة متغيّرات، ومتغيّرَين من أعضاء
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
.
- أنشئ طريقة باسم
createRepository
وأضِف إليها التعليق التوضيحي@Before
. - أنشئ مثيلاً لمصادر البيانات الوهمية باستخدام القائمتَين
remoteTasks
وlocalTasks
. - أنشئ مثيلاً من
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
!
- اكتب اختبارًا لطريقة
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 للتعامل مع تشغيل الروتينات الفرعية في اختباراتك.
- أضِف التبعيات المطلوبة لاختبار الروتينات الفرعية إلى مجموعة مصادر الاختبار باستخدام
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
لا تنسَ المزامنة!
kotlinx-coroutines-test
هي مكتبة اختبارات الروتينات الفرعية، وهي مخصّصة تحديدًا لاختبار الروتينات الفرعية. لتشغيل الاختبارات، استخدِم الدالة runBlockingTest
. هذه دالة توفّرها مكتبة اختبار الروتينات الفرعية. تتلقّى هذه الدالة مجموعة من الرموز البرمجية ثم تشغّلها في سياق خاص للروتين الفرعي يعمل بشكل متزامن وفوري، ما يعني أنّ الإجراءات ستحدث بترتيب محدد. يؤدي ذلك بشكل أساسي إلى تشغيل الروتينات الفرعية كما لو كانت غير متزامنة، لذا فهي مخصّصة لاختبار الرموز البرمجية.
استخدِم runBlockingTest
في صفوف الاختبار عند استدعاء الدالة suspend
. ستتعرّف على مزيد من المعلومات حول طريقة عمل runBlockingTest
وكيفية اختبار الروتينات المشتركة في الدرس العملي التالي ضمن هذه السلسلة.
- أضِف
@ExperimentalCoroutinesApi
فوق الصف. يشير ذلك إلى أنّك على عِلم بأنّك تستخدم واجهة برمجة تطبيقات تجريبية لبرامج فرعية (runBlockingTest
) في الفئة. بدونه، ستتلقّى تحذيرًا. - في
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))
}
}
- نفِّذ اختبار
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
.
- افتح
DefaultTasksRepository
وانقر بزر الماوس الأيمن على اسم الصف. بعد ذلك، اختَر إعادة تصميم -> استخراج -> الواجهة.
- اختَر استخراج إلى ملف منفصل.
- في نافذة استخراج الواجهة، غيِّر اسم الواجهة إلى
TasksRepository
. - في قسم الأعضاء لتشكيل الواجهة، ضَع علامة في المربّع بجانب جميع الأعضاء باستثناء العضوَين المساعدَين والطُرق الخاصة.
- انقر على إعادة تصميم. من المفترض أن تظهر واجهة
TasksRepository
الجديدة في حزمة data/source .
وتنفّذ DefaultTasksRepository
الآن TasksRepository
.
- شغِّل تطبيقك (وليس الاختبارات) للتأكّد من أنّ كل شيء لا يزال يعمل بشكل سليم.
الخطوة 2: Create FakeTestRepository
بعد توفّر الواجهة، يمكنك إنشاء DefaultTaskRepository
بديل الاختبار.
- في مجموعة مصادر الاختبار، أنشئ ملف Kotlin وفئة
FakeTestRepository.kt
في data/source، ثم وسِّع نطاقها من واجهةTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
سيُطلب منك تنفيذ طرق الواجهة.
- مرِّر مؤشر الماوس فوق الخطأ إلى أن تظهر قائمة الاقتراحات، ثم انقر على تنفيذ الأعضاء واختَرها.
- اختَر جميع الطرق واضغط على حسنًا.
الخطوة 3: تنفيذ طرق FakeTestRepository
أصبح لديك الآن فئة FakeTestRepository
تتضمّن طرقًا "لم يتم تنفيذها". على غرار طريقة تنفيذ FakeDataSource
، سيتم دعم FakeTestRepository
ببنية بيانات، بدلاً من التعامل مع عملية توسّط معقّدة بين مصادر البيانات المحلية والبعيدة.
يُرجى العِلم أنّ FakeTestRepository
لا يحتاج إلى استخدام FakeDataSource
أو أي شيء من هذا القبيل، بل يحتاج فقط إلى عرض نواتج وهمية واقعية عند إدخال بيانات. ستستخدم LinkedHashMap
لتخزين قائمة المهام وMutableLiveData
للمهام القابلة للمراقبة.
- في
FakeTestRepository
، أضِف متغيّرLinkedHashMap
يمثّل قائمة المهام الحالية وMutableLiveData
للمهام القابلة للمراقبة.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
نفِّذ الطرق التالية:
getTasks
: يجب أن تأخذ هذه الطريقةtasksServiceData
وتحوّله إلى قائمة باستخدامtasksServiceData.values.toList()
، ثم تعرضه كنتيجةSuccess
.refreshTasks
: تعدّل قيمةobservableTasks
لتصبح القيمة التي تعرضهاgetTasks()
.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
عدة مرات، ولكن لتسهيل ذلك، أضِف طريقة مساعدة مخصّصة للاختبارات تتيح لك إضافة مهام.
- أضِف طريقة
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
.
- افتح
TasksViewModel
. - غيِّر أداة الإنشاء الخاصة بالفئة
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
، ولكن يمكنك أيضًا وضعها في ملف منفصل.
- في أسفل ملف
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
. بعد إنشاء المصنع، استخدِمه أينما تنشئ نموذج العرض.
- يجب تحديث
TasksFragment
لاستخدام المصنع.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- نفِّذ رمز التطبيق وتأكَّد من أنّ كل شيء لا يزال يعمل.
الخطوة 2: استخدام FakeTestRepository داخل TasksViewModelTest
بدلاً من استخدام المستودع الحقيقي في اختبارات نموذج العرض، يمكنك الآن استخدام المستودع الزائف.
- افتح
TasksViewModelTest
. - أضِف موقعًا إلكترونيًا على
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
}
- عدِّل الطريقة
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)
}
- بما أنّك لم تعُد تستخدم رمز AndroidX Test
ApplicationProvider.getApplicationContext
، يمكنك أيضًا إزالة التعليق التوضيحي@RunWith(AndroidJUnit4::class)
. - نفِّذ اختباراتك وتأكَّد من أنّها لا تزال تعمل.
باستخدام ميزة "إدخال التبعية" في الدالة الإنشائية، تكون قد أزلت الآن DefaultTasksRepository
كعنصر تابع واستبدلته بـ FakeTestRepository
في الاختبارات.
الخطوة 3: تعديل الفئة TaskDetail Fragment وViewModel أيضًا
أجرِ التغييرات نفسها بالضبط على TaskDetailFragment
وTaskDetailViewModel
. سيؤدي ذلك إلى إعداد الرمز عندما تكتب TaskDetail
اختبارات في الخطوة التالية.
- افتح
TaskDetailViewModel
. - عدِّل الدالة الإنشائية:
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 }
- في أسفل ملف
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)
}
- يجب تحديث
TasksFragment
لاستخدام المصنع.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- شغِّل الرمز وتأكَّد من أنّ كل شيء يعمل بشكل صحيح.
يمكنك الآن استخدام FakeTestRepository
بدلاً من المستودع الفعلي في TasksFragment
وTasksDetailFragment
.
بعد ذلك، ستكتب اختبارات تكامل لاختبار تفاعلات الأجزاء ونماذج العرض. ستعرف ما إذا كان رمز نموذج العرض يحدّث واجهة المستخدم بشكلٍ مناسب. لإجراء ذلك، يمكنك استخدام
- نمط ServiceLocator
- مكتبتَي Espresso وMockito
تختبر اختبارات الدمج تفاعل عدة فئات للتأكّد من أنّها تتصرف على النحو المتوقّع عند استخدامها معًا. يمكن إجراء هذه الاختبارات إما على الجهاز (مجموعة المصادر test
) أو كاختبارات أدوات (مجموعة المصادر androidTest
).
في حالتك، ستأخذ كل جزء وتكتب اختبارات تكامل للجزء ونموذج العرض لاختبار الميزات الرئيسية للجزء.
الخطوة 1: إضافة تبعيات Gradle
- أضِف تبعيات 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
لأنّه يتضمّن وظائف أساسية مقارنةً بالأجزاء الأخرى.
- افتح
taskdetail.TaskDetailFragment
. - أنشئ اختبارًا لـ
TaskDetailFragment
، كما فعلت من قبل. اقبل الخيارات التلقائية وضَعها في مجموعة المصادر androidTest (وليس مجموعة المصادرtest
).
- أضِف التعليقات التوضيحية التالية إلى فئة
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
).
- انسخ هذا الاختبار إلى
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
باستخدام هذه الحزمة والمظهر.
هذا ليس اختبارًا مكتملاً بعد، لأنّه لا يؤكّد أي شيء. في الوقت الحالي، نفِّذ الاختبار ولاحظ ما يحدث.
- هذا اختبار مزوّد بأدوات، لذا تأكَّد من أنّ المحاكي أو جهازك مرئي.
- نفِّذ الاختبار.
من المفترض أن يحدث ما يلي:
- أولاً، بما أنّ هذا الاختبار يتضمّن أدوات، سيتم تنفيذه إما على جهازك الفعلي (في حال كان متصلاً) أو على محاكي.
- من المفترض أن يتم تشغيل الجزء.
- لاحظ كيف أنّه لا يتنقّل بين أي أجزاء أخرى أو يتضمّن أي قوائم مرتبطة بالنشاط، بل هو مجرد جزء.
أخيرًا، انتبه جيدًا ولاحظ أنّ الجزء يعرض الرسالة "لا تتوفّر بيانات" لأنّه لم يتم تحميل بيانات المهمة بنجاح.
يجب أن يتم تحميل 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 | استخدام أداة تحديد موقع الخدمة |
بالنسبة إلى تطبيق هذا الدرس العملي، اتّبِع الخطوات التالية:
- أنشئ فئة Service Locator يمكنها إنشاء مستودع وتخزينه. ينشئ تلقائيًا مستودعًا "عاديًا".
- أعِد تصميم الرمز البرمجي بحيث تستخدم Service Locator عندما تحتاج إلى مستودع.
- في فئة الاختبار، استدعِ طريقة في Service Locator تستبدل المستودع "العادي" ببديل الاختبار.
الخطوة 1: إنشاء ServiceLocator
لننشئ فئة ServiceLocator
. سيتم تخزينها في مجموعة المصادر الرئيسية مع بقية رموز التطبيق لأنّها مستخدَمة من خلال رمز التطبيق الرئيسي.
ملاحظة: ServiceLocator
هو عنصر فردي، لذا استخدِم الكلمة الرئيسية object
في Kotlin للفئة.
- أنشئ الملف ServiceLocator.kt في المستوى الأعلى من مجموعة المصادر الرئيسية.
- عرِّف
object
باسمServiceLocator
. - أنشئ متغيرَي مثيل
database
وrepository
واضبط قيمتيهما علىnull
. - أضِف التعليق التوضيحي
@Volatile
إلى المستودع لأنّه يمكن أن تستخدمه سلاسل محادثات متعددة (يتم شرح@Volatile
بالتفصيل هنا).
يجب أن يبدو الرمز البرمجي كما هو موضّح أدناه.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
في الوقت الحالي، كل ما يجب أن تفعله ServiceLocator
هو معرفة كيفية عرض TasksRepository
. سيعرض DefaultTasksRepository
حاليًا أو سينشئ DefaultTasksRepository
جديدًا ويعرضه إذا لزم الأمر.
حدِّد الدوال التالية:
provideTasksRepository
: يوفّر مستودعًا حاليًا أو ينشئ مستودعًا جديدًا. يجب أن تكون هذه الطريقةsynchronized
فيthis
لتجنُّب إنشاء مثيلَين للمستودع عن طريق الخطأ في الحالات التي يتم فيها تشغيل سلاسل محادثات متعددة.createTasksRepository
: رمز لإنشاء مستودع جديد سيتم الاتصال بـcreateTaskLocalDataSource
وإنشاءTasksRemoteDataSource
جديد.createTaskLocalDataSource
: رمز لإنشاء مصدر بيانات محلي جديد. سيتم الاتصال بـ createDataBase
.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
.
من المهم ألا تنشئ سوى مثيل واحد لفئة المستودع. لضمان ذلك، ستستخدم أداة تحديد موقع الخدمة في فئة التطبيق.
- في أعلى مستوى من تسلسل حِزمك الهرمي، افتح
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
.
- افتح
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
بدلاً من إنشاء المستودع مباشرةً.
- افتح
TaskDetailFragement
وابحث عن الاتصال بـgetRepository
في أعلى الصف. - استبدِل هذه المكالمة بمكالمة تحصل على المستودع من
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)
}
- كرِّر الأمر نفسه مع
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)
}
- بالنسبة إلى
StatisticsViewModel
وAddEditTaskViewModel
، عدِّل الرمز الذي يحصل على المستودع لاستخدام المستودع منTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- شغِّل تطبيقك (وليس الاختبار).
بما أنّك أعدت تصميم الرمز فقط، من المفترض أن يعمل التطبيق بالطريقة نفسها بدون أي مشاكل.
الخطوة 3: Create FakeAndroidTestRepository
لديك FakeTestRepository
في مجموعة مصادر الاختبار. لا يمكنك مشاركة فئات الاختبار بين مجموعتَي المصادر test
وandroidTest
تلقائيًا. لذا، عليك إنشاء نسخة مكرّرة من فئة FakeTestRepository
في مجموعة المصادر androidTest
، وتسميتها FakeAndroidTestRepository
.
- انقر بزرّ الماوس الأيمن على مجموعة المصادر
androidTest
وأنشئ حزمة بيانات. انقر بزر الماوس الأيمن مرة أخرى وأنشئ حزمة مصدر . - أنشئ فئة جديدة في حزمة المصدر هذه باسم
FakeAndroidTestRepository.kt
. - انسخ الرمز التالي إلى هذا الصف.
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
.
- افتح
ServiceLocator.kt
. - ضَع علامة
@VisibleForTesting
على الدالة الضابطة للسمةtasksRepository
. هذا التعليق التوضيحي هو طريقة للتعبير عن أنّ سبب إتاحة أداة الضبط للجميع هو الاختبار.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
سواء أجريت الاختبار بمفردك أو في مجموعة من الاختبارات، يجب أن يتم تشغيل اختباراتك بالطريقة نفسها تمامًا. ويعني ذلك أنّه يجب ألا تتضمّن اختباراتك أي سلوك يعتمد على بعضها البعض (ما يعني تجنُّب مشاركة العناصر بين الاختبارات).
بما أنّ ServiceLocator
هو عنصر فردي، من المحتمل أن تتم مشاركته عن طريق الخطأ بين الاختبارات. للمساعدة في تجنُّب ذلك، أنشئ طريقة تعيد ضبط حالة ServiceLocator
بشكلٍ سليم بين الاختبارات.
- أضِف متغيّرًا مثيلاً باسم
lock
بالقيمةAny
.
ServiceLocator.kt
private val lock = Any()
- أضِف طريقة خاصة بالاختبار تُسمى
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
.
- افتح
TaskDetailFragmentTest
. - حدِّد متغيّر
lateinit TasksRepository
. - أضِف طريقتَي إعداد وإزالة لإعداد
FakeAndroidTestRepository
قبل كل اختبار وإزالته بعد كل اختبار.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- لفّ نص الدالة
activeTaskDetails_DisplayedInUi()
فيrunBlockingTest
- احفظ
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)
}
- إضافة تعليقات توضيحية إلى الصف بأكمله باستخدام
@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)
}
}
- أجرِ اختبار
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، من أفضل الممارسات إيقاف الرسوم المتحركة (سيتم أيضًا تشغيل الاختبار بشكل أسرع):
- على جهاز الاختبار، انتقِل إلى الإعدادات > خيارات المطوّرين.
- أوقِف الإعدادات الثلاثة التالية: حجم الرسوم المتحركة للنافذة وحجم الرسوم المتحركة للنقل وطول مدة الرسوم المتحركة.
الخطوة 3: إلقاء نظرة على اختبار Espresso
قبل كتابة اختبار Espresso، ألقِ نظرة على بعض رموز Espresso البرمجية.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
يؤدي هذا البيان إلى العثور على طريقة عرض مربّع الاختيار الذي يحمل المعرّف task_detail_complete_checkbox
، والنقر عليه، ثم التأكّد من أنّه تم وضع علامة فيه.
تتألف غالبية عبارات 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
هو إجراء يمكن تنفيذه على العرض، مثلاً النقر على العرض.
check(matches(isChecked()))
check
التي تستغرق ViewAssertion
. تتحقّق ViewAssertion
s من شيء ما بشأن طريقة العرض أو تؤكّده. إنّ ViewAssertion
الأكثر شيوعًا الذي ستستخدمه هو تأكيد matches
. لإنهاء التأكيد، استخدِم ViewMatcher
آخر، وهو isChecked
في هذه الحالة.
يُرجى العِلم أنّه ليس عليك دائمًا استدعاء كل من perform
وcheck
في عبارة Espresso. يمكنك تضمين عبارات تؤكّد على صحة معلومة باستخدام check
أو عبارات تنفّذ إجراءً باستخدام ViewAction
من خلال perform
.
- افتح
TaskDetailFragmentTest.kt
. - تعديل اختبار
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
- يستخدم كل ما يلي التعليق
// THEN
Espresso. افحص بنية الاختبار واستخدامwithId
وتحقّق من صحة التأكيدات بشأن الشكل الذي يجب أن تبدو عليه صفحة التفاصيل. - نفِّذ الاختبار وتأكَّد من اجتيازه.
الخطوة 4: اختياري، كتابة اختبار Espresso الخاص بك
الآن، اكتب اختبارًا بنفسك.
- أنشئ اختبارًا جديدًا باسم
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
}
- بالنظر إلى الاختبار السابق، أكمِل هذا الاختبار.
- نفِّذ الاختبار وتأكَّد من اجتيازه.
يجب أن يبدو 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
- أضِف تبعيات 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
- فتح "
TasksFragment
" - انقر بزر الماوس الأيمن على اسم الفئة
TasksFragment
، ثم اختَر إنشاء (Generate) ثم اختبار (Test). أنشئ اختبارًا في مجموعة المصادر androidTest. - انسخ هذا الرمز إلى
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
الصحيح.
- أضِف الاختبار
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)
}
- استخدِم الدالة
mock
في Mockito لإنشاء عنصر وهمي.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
لإنشاء عنصر زائف في Mockito، عليك إدخال الفئة التي تريد إنشاء عنصر زائف لها.
بعد ذلك، عليك ربط NavController
بالجزء. تتيح لك السمة onFragment
استدعاء طرق في الجزء نفسه.
- اجعل العنصر الجديد
NavController
الخاص بالجزء.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- أضِف الرمز للنقر على العنصر في
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.
- تأكَّد من أنّه تم استدعاء
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")
)
}
- إجراء الاختبار
باختصار، لاختبار التنقّل، يمكنك اتّباع الخطوات التالية:
- استخدِم Mockito لإنشاء
NavController
وهمي. - أرفِق هذا العنصر
NavController
الوهمي بالجزء. - تأكَّد من أنّ الدالة navigate تم استدعاؤها باستخدام الإجراء والمعلَمات الصحيحة.
الخطوة 3: اختياري، اكتب clickAddTaskButton_navigateToAddEditFragment
لمعرفة ما إذا كان بإمكانك كتابة اختبار تنقّل بنفسك، جرِّب هذه المهمة.
- اكتب اختبار
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.
تناول هذا الدرس التطبيقي حول الترميز كيفية إعداد ميزة "توفير التبعية" يدويًا، وميزة "محدد موقع الخدمة"، وكيفية استخدام "النماذج التجريبية" و"النماذج الوهمية" في تطبيقات Android Kotlin. وعلى وجه الخصوص:
- يحدّد ما تريد اختباره واستراتيجية الاختبار أنواع الاختبارات التي ستنفّذها لتطبيقك. اختبارات الوحدات مركّزة وسريعة. تتحقّق اختبارات الدمج من التفاعل بين أجزاء برنامجك. تتحقّق الاختبارات الشاملة من الميزات، وتتسم بأعلى دقة، ويتم غالبًا تزويدها بأدوات القياس، وقد يستغرق تنفيذها وقتًا أطول.
- تؤثر بنية تطبيقك في مدى صعوبة اختباره.
- تطوير يستند إلى الاختبار (TDD) هو استراتيجية تكتب فيها الاختبارات أولاً، ثم تنشئ الميزة لاجتياز الاختبارات.
- لعزل أجزاء من تطبيقك بغرض الاختبار، يمكنك استخدام عناصر بديلة للاختبار. عنصر الاختبار البديل هو نسخة من فئة مصمَّمة خصيصًا للاختبار. على سبيل المثال، يمكنك محاكاة الحصول على بيانات من قاعدة بيانات أو الإنترنت.
- استخدِم تضمين التبعية لاستبدال فئة حقيقية بفئة اختبار، مثل مستودع أو طبقة شبكة.
- استخدِم الاختبارات المزوّدة بأدوات (
androidTest
) لتشغيل مكوّنات واجهة المستخدم. - عندما لا يمكنك استخدام ميزة "تضمين التبعية في الدالة الإنشائية"، مثلاً لتشغيل جزء، يمكنك غالبًا استخدام أداة تحديد موقع الخدمة. نمط تحديد موقع الخدمة هو بديل لنمط إدخال التبعية. يتضمّن ذلك إنشاء فئة ذات مثيل واحد تُعرف باسم "محدد الموقع الجغرافي للخدمة"، والغرض منها هو توفير التبعيات، لكلّ من الرمز العادي ورمز الاختبار.
دورة Udacity التدريبية:
مستندات مطوّري تطبيقات Android:
- دليل حول بنية التطبيق
runBlocking
وrunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- مكتبة الاختبار AndroidX
- مكتبة الاختبار الأساسية لمكوّنات AndroidX Architecture
- مجموعات المستندات المصدر
- الاختبار من سطر الأوامر
فيديوهات:
غير ذلك:
للحصول على روابط تؤدي إلى دروس برمجية أخرى في هذه الدورة التدريبية، يمكنك الانتقال إلى الصفحة المقصودة للدروس البرمجية المتقدّمة حول Android بلغة Kotlin.