يُعد هذا الدرس التطبيقي جزءًا من الدورة التدريبية المتقدّمة لنظام التشغيل Android في لغة Kotlin. ستحصل على أقصى استفادة من هذه الدورة التدريبية إذا كنت تعمل من خلال الدروس التطبيقية حول الترميز بالتسلسل، ولكن هذا ليس إلزاميًا. يتم إدراج جميع الدروس التطبيقية حول ترميز الدورات التدريبية في الصفحة المقصودة لبرنامج Android المتقدّم في لغة ترميز Kotlin.
مقدمة
يتناول هذا الدرس التطبيقي الثاني حول الترميز الاستعانة بمضاعفات الاختبار: وقت استخدامها في Android، وكيفية تنفيذها باستخدام إدخال التبعية ونمط محدِّد مواقع الخدمة والمكتبات من خلال إجراء ذلك، ستتعرّف على كيفية الكتابة:
- اختبارات وحدة المستودع
- اختبارات الدمج ونموذج العرض
- اختبارات التنقل المجزأة
ما يجب معرفته
ويجب أن تكون على دراية بما يلي:
- لغة البرمجة Kotlin
- مفاهيم الاختبار التي يتناولها الدرس التطبيقي الأول حول الترميز: كتابة اختبارات الوحدات وتنفيذها على نظام التشغيل Android، باستخدام JUnit وهامكرست واختبار AndroidX وRobolectric بالإضافة إلى Test LiveData
- مكتبات Jetpack الأساسية التالية:
ViewModel
وLiveData
ومكوّن التنقُّل - بنية التطبيق، باتباع النمط من الدليل إلى بنية التطبيق والدروس التطبيقية حول ترميز أساسيات Android
- أساسيات الكوروتين على Android
ما ستتعرَّف عليه
- كيفية التخطيط لاستراتيجية اختبار
- كيفية إنشاء واستخدام زوجيات الاختبار، أي المنتجات المزيفة والزائفة
- كيفية استخدام إدخال الاعتمادية على Android لاختبارات الوحدات والدمج
- كيفية تطبيق نمط محدّد مواقع الخدمات
- كيفية اختبار المستودعات والأجزاء ونماذج العرض ومكوّن التنقل
وستستخدم المكتبات ومفاهيم الرموز التالية:
الإجراءات التي ستنفذّها
- كتابة اختبارات الوحدات للمستودع باستخدام اختبار التبعية المزدوج والاعتمادية.
- كتابة اختبارات الوحدات لنموذج العرض باستخدام اختبار مزدوج والاعتمادية.
- كتابة اختبارات الدمج للأجزاء ونماذج العرض باستخدام إطار عمل اختبار واجهة مستخدم Espresso.
- اكتب اختبارات التنقل باستخدام Mockito وEspresso.
في هذه السلسلة من الدروس التطبيقية حول الترميز، ستعمل مع تطبيق الملاحظات في قائمة المهام. ويتيح لك التطبيق تدوين المهام لإكمالها وعرضها في قائمة. ويمكنك بعد ذلك وضع علامة "مكتملة" أو "غير مكتملة" أو فلترتها أو حذفها.
هذا التطبيق مكتوب بلغة Kotlin ويحتوي على بعض الشاشات ويستخدم مكونات Jetpack ويتّبع البنية من دليل هندسة التطبيقات. من خلال تعلّم كيفية اختبار هذا التطبيق، ستتمكّن من اختبار التطبيقات التي تستخدم المكتبات والبنية نفسها.
تنزيل الرمز
للبدء، يُرجى تنزيل الرمز:
بدلاً من ذلك، يمكنك إنشاء نسخة طبق الأصل من مستودع Github للرمز:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
يُرجى تخصيص بعض الوقت للتعرّف على الرمز، باتّباع التعليمات الواردة أدناه.
الخطوة 1: تشغيل نموذج التطبيق
بعد تنزيل تطبيق المهام المطلوبة، افتحه في Android Studio وشغِّله. يجب أن يتم تجميعها. استكشِف التطبيق من خلال إجراء ما يلي:
- يمكنك إنشاء مهمة جديدة باستخدام الزر عائم للإجراء. أدخِل عنوانًا أولاً، ثم أدخِل معلومات إضافية عن المهمة. احفظه باستخدام علامة الاستفهام FAB التي تشير إلى اللون الأخضر.
- في قائمة المهام، انقر على عنوان المهمة التي أكملتها وألقِ نظرة على شاشة تفاصيل تلك المهمة للاطّلاع على بقية الوصف.
- في القائمة أو على شاشة التفاصيل، ضع علامة في مربّع اختيار هذه المهمة لضبط حالتها على مكتملة.
- ارجع إلى شاشة المهام، وافتح قائمة الفلاتر، وفلتِر المهام حسب الحالة نشط ومكتمل.
- افتح لائحة التنقل وانقر على الإحصاءات.
- ارجع إلى شاشة النظرة العامة، ومن قائمة درج التنقّل، اختَر محو المهام المكتملة لحذف جميع المهام بالحالة مكتملة.
الخطوة 2: استكشاف نموذج رمز التطبيق
يستند تطبيق قائمة المهام إلى نموذج الهندسة المعمارية الزرقاء الشائعة واختباره (باستخدام إصدار البنية التفاعلية من النموذج). يتّبع التطبيق البنية من دليل إلى بنية التطبيق. وهي تستخدم ViewViews مع أجزاء ومستودع وغرفة. إذا كنت على دراية بأي من الأمثلة التالية، يكون لهذا التطبيق بنية مشابهة:
- غرفة بدرس تطبيقي حول الترميز
- الدروس التطبيقية حول ترميز أساسيات Android Kotlin
- الدروس التطبيقية التدريبية المتقدّمة حول ترميز Android
- نموذج دوار الشمس لنظام التشغيل Android
- تطوير تطبيقات متوافقة مع Android باستخدام دورة تدريبية على لغة Kotlin Udacity
من المهم فهم البنية العامة للتطبيق بدلاً من فهم عميق للمنطق في أي طبقة.
في ما يلي ملخص الحِزم التي ستظهر لك:
الحزمة: | |
| إضافة شاشة مهمة أو تعديلها: رمز طبقة واجهة المستخدم لإضافة مهمة أو تعديلها. |
| طبقة البيانات: تتعامل هذه الطبقة مع طبقة بيانات المهام. ويحتوي هذا المورد على قاعدة البيانات والشبكة ورمز المستودع. |
| شاشة الإحصاءات: رمز طبقة واجهة المستخدِم لشاشة الإحصاءات. |
| شاشة تفاصيل المهمة: رمز طبقة واجهة المستخدم لمَهمة واحدة. |
| شاشة المهام: رمز طبقة واجهة المستخدم لقائمة جميع المهام. |
| فئات الأدوات المساعدة: الصفوف المشتركة المستخدمة في أجزاء مختلفة من التطبيق، مثلاً لتنسيق التمرير السريع الذي يتم استخدامه في شاشات متعددة. |
طبقة البيانات (data.)
يتضمن هذا التطبيق طبقة محاكاة للشبكة، في حزمة بعيد، وطبقة قاعدة بيانات، في الحزمة المحلية. لتبسيط الأمر، تمت في هذا المشروع محاكاة طبقة الشبكة باستخدام HashMap
فقط مع تأخير، بدلاً من تقديم طلبات شبكة حقيقية.
إحداثيات DefaultTasksRepository
أو وسيطات بين طبقة الاتصال وطبقة قاعدة البيانات وهي ما يعرض البيانات إلى طبقة واجهة المستخدم.
طبقة واجهة المستخدم ( .addedittask, .statistics, .taskdetail, .tasks)
وتحتوي كل حزمة من حزم طبقة واجهة المستخدم على جزء ونموذج عرض، إلى جانب أي فئات أخرى مطلوبة لواجهة المستخدم (مثل محوِّل لقائمة المهام). TaskActivity
هي النشاط الذي يتضمّن جميع الأجزاء.
التنقل
يتم التحكم في التنقل داخل التطبيق من خلال مكوّن التنقل. يتم تحديد ذلك في ملف nav_graph.xml
. يتم تشغيل التنقّل في نماذج الملف الشخصي باستخدام فئة Event
، كما تحدّد نماذج العرض الوسيطات التي يجب تمريرها. وتلاحظ الأجزاء Event
وهي تنفّذ عمليات التنقل الفعلية بين الشاشات.
في هذا الدرس التطبيقي حول الترميز، ستتعلّم كيفية اختبار المستودعات وعرض النماذج وأجزاء الأجزاء باستخدام العناصر المكرّرة للاختبار وإدخالات التبعية. قبل الاطّلاع على تفاصيل تلك الاختبارات، من المهمّ فهم السبب الذي سيوجّهك إلى كيفية إجراء هذه الاختبارات وكيفية كتابتها.
يتناول هذا القسم بعضًا من أفضل ممارسات الاختبار بشكل عام، حيث تنطبق على Android.
هرم الاختبار
عند التفكير في استراتيجية للاختبار، هناك ثلاثة جوانب ذات صلة للاختبار:
- النطاق: ما مقدار الرمز الذي يلمسه الاختبار؟ يمكن إجراء الاختبارات بطريقة واحدة أو عبر التطبيق بالكامل أو في مكان ما بينهما.
- السرعة: ما مدى سرعة إجراء الاختبار؟ ويمكن أن تتراوح سرعات الاختبار من مللي ثانية إلى عدة دقائق.
- الدقّة: كيف يتم إجراء الاختبار؟ على سبيل المثال، إذا كان جزء من الرمز الذي تريد اختباره بحاجة إلى تقديم طلب شبكة، هل يؤدي رمز الاختبار إلى طلب الشبكة هذا، أو هل يؤدي إلى تزوير النتيجة؟ إذا كان الاختبار يتحدث فعليًا مع الشبكة، فهذا يعني أن الدقّة العالية. وخيار المقايضة هو أن الاختبار قد يستغرق وقتًا أطول، وقد يؤدي إلى حدوث أخطاء إذا كانت الشبكة معطّلة أو قد يكون مكلفًا للاستخدام.
وهناك مفاضلات بطيئة بين هذه الجوانب. على سبيل المثال، تعتبر السرعة والدقة مقايضة، إذ كلما ازدادت سرعة الاختبار وبصفة عامة، قلّت الدقّة والعكس صحيح. هناك طريقة شائعة لتقسيم الاختبارات التلقائية إلى الفئات الثلاث التالية:
- اختبارات الوحدة: هذه اختبارات عالية التركيز يتم تنفيذها على صف واحد، وعادةً ما تكون طريقة واحدة في ذلك الصف. إذا تعذّر اختبار الوحدة، يمكنك معرفة مكان المشكلة في الرمز بالضبط. نظرًا لدقّتها المنخفضة في الواقع، يتضمّن تطبيقك أكثر من تنفيذ طريقة أو فئة واحدة. وهي سريعة بما يكفي للتشغيل في كل مرة تغير فيها الرمز. وغالبًا ما يتم إجراء الاختبارات محليًا (في مجموعة مصادر
test
). مثال: اختبار طرق فردية في نماذج العرض والمستودعات. - اختبارات الدمج - تعمل هذه على اختبار التفاعل لعدة صفوف للتأكُّد من أنها تعمل بالشكل المتوقع عند استخدامها معًا. وتتمثل إحدى طرق تنظيم اختبارات الدمج في اختبار ميزة واحدة، مثل القدرة على حفظ المهمة. تختبر هذه الاختبارات نطاقًا أوسع من الترميز مقارنةً باختبارات الوحدة، ولكنها لا تزال مُحسَّنة للتشغيل بسرعة، مقارنةً بالدقة الكاملة. ويمكن تنفيذها محليًا أو كاختبارات على قياس الأداء، اعتمادًا على الحالة. مثال: اختبار جميع وظائف جزء واحد وعرض زوج النماذج.
- الاختبارات النهائية (E2e): يمكنك اختبار تركيبة من الميزات التي تعمل معًا. وتختبر هذه الميزة أجزاءً كبيرة من التطبيق وتحاكي الاستخدام الفعلي عن كثب، وبالتالي تكون عادةً بطيئة. وتتميّز هذه التطبيقات بأعلى مستوى من الدقة، وتخبرك بأن تطبيقك يعمل بشكل عام. على نطاق واسع، سيتم إجراء هذه الاختبارات على شكل اختبارات (في مجموعة المصدر
androidTest
)
مثال: بدء تشغيل التطبيق بالكامل واختبار بعض الميزات معًا.
وتمثّل النسبة المقترَحة من هذه الاختبارات في الغالب هرمًا تمثّل غالبية اختباراتها الغالبية العظمى.
الهندسة المعمارية والاختبار
قدرتك على اختبار تطبيقك على جميع المستويات المختلفة للهرم الاختباري ترتبط بطبيعتها بتصميم تطبيقك. على سبيل المثال، قد يضع تطبيق شديد التطور بنية سيئة جميع منطقه ضمن طريقة واحدة. قد تتمكّن من كتابة اختبار شامل لهذا الغرض، لأنّ هذه الاختبارات تميل إلى اختبار أجزاء كبيرة من التطبيق، ولكن ماذا عن كتابة وحدات أو اختبارات تكامل؟ مع كل الشفرة في مكان واحد، يكون من الصعب اختبار الرمز المرتبط بوحدة أو ميزة واحدة.
منهج أفضل هو تقسيم منطق التطبيق إلى طرق وصفوف متعددة، مما يسمح باختبار كل جزء على حدة. البنية هي طريقة لتقسيم التعليمات البرمجية وتنظيمها، مما يسمح باختبار الوحدات والدمج بسهولة. يتبع تطبيق المهام الذي سيتم اختباره بنية معينة:
في هذا الدرس، ستتعرّف على كيفية اختبار أجزاء من البنية أعلاه باستخدام عزل مناسب:
- عليك أولاً اختبار الوحدة للمستودع.
- وبعد ذلك، ستستخدم اختبارًا مزدوجًا في نموذج العرض، وهو إجراء ضروري لاختبار الوحدة واختبار الدمج لنموذج العرض.
- بعد ذلك، ستتعرّف على كيفية كتابة اختبارات الدمج للأجزاء ونماذج العرض.
- أخيرًا، ستتعرّف على كيفية كتابة اختبارات الدمج التي تتضمّن مكوّن التنقّل.
وسيتم تناول الاختبارات الشاملة في الدرس التالي.
عند كتابة اختبار وحدة لجزء من صف (طريقة أو مجموعة صغيرة من الطرق)، يكون هدفك هو اختبار الرمز في ذلك الصف فقط.
قد يكون اختبار الرمز فقط في صف أو صفوف معينة أمرًا صعبًا. لنلقِ نظرة على أحد الأمثلة. افتح الصف 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 للحصول على بيئة Android افتراضية.
- قد يستغرق تشغيل بعض أجزاء من الرمز، مثل رمز الشبكة، وقتًا طويلاً، أو قد يتعذّر أحيانًا إتمامها، ما يؤدي إلى إجراء اختبارات غير مستقرة لمدة طويلة.
- قد تفقد اختباراتك قدرتها على تحديد الرمز الذي يخطئ في حال تعذّر إجراء الاختبار. قد تبدأ اختباراتك باختبار رمز غير مستودعي، لذا على سبيل المثال، قد يتعذّر إجراء اختبارات الوحدات المُفترضة والتعبيرية عن مشكلة بسبب مشكلة في بعض الرموز التابعة، مثل رمز قاعدة البيانات.
اختبار زوجي
ويتمثل حل هذه المشكلة في أنه عندما تختبر الاختبار، لا تستخدم الرمز الفعلي لقاعدة البيانات أو قاعدة البيانات، بل استخدِم اختبارًا مزدوجًا بدلاً من ذلك. الاختبار المزدوج هو إصدار من صف تم تصميمه خصيصًا للاختبار. ويهدف إلى استبدال النسخة الحقيقية لأحد الصفوف في الاختبارات. ويشبه ذلك كيفية تنفيذ حركات الحركات البهلوانية المميزة التي يتخصّص في أداء حركات بهلوانية، كما يستبدل الممثل الحقيقي بالإجراءات الخطيرة.
في ما يلي بعض أنواع الاختبارات المزدوجة:
تزييف | اختبار مزدوج يحتوي على ن&ظارة "عمل"؛ وتنفيذ؛ للصف، ولكن تم تنفيذه بطريقة تجعله مناسبًا للاختبارات ولكنه غير مناسب للإنتاج. |
المحاكاة | اختبار مزدوج يتتبّع طريقة استدعاء أي من هذه الطرق بعد ذلك يتم اجتياز الاختبار أو تعذّر استنادًا إلى ما إذا تم استدعاء الطرق بشكل صحيح. |
رمز بديل | اختبار ثنائي لا يتضمن أي منطق ولا يعرض سوى ما تتم برمجته من أجل عرضه. يمكن مثلاً برمجة |
دمية | اختبار تم اجتيازه مرتين، ولكن لم يتم استخدامه، مثل إذا كنت تحتاج إلى تقديمه كمعلمة. أمّا إذا استخدمت |
جاسوس | اختبار مزدوج يتتبع أيضًا بعض المعلومات الإضافية. على سبيل المثال، إذا أنشأت |
للحصول على مزيد من المعلومات حول الاختبارات المزدوجة، اطّلِع على الاختبار على المرحاض: التعرّف على زوجي في اختبارك.
يتمثل الاختبار المزدوج الأكثر استخدامًا في Android في Fakes وMocks.
في هذه المهمة، ستنشئ اختبار FakeDataSource
مزدوجًا لاختبار الوحدة DefaultTasksRepository
منفصلاً عن مصادر البيانات الفعلية.
الخطوة 1: إنشاء صف FakeDataSource
في هذه الخطوة، ستنشئ صفًا باسم FakeDataSouce
، والذي سيكون اختبارًا لمضاعفة LocalDataSource
وRemoteDataSource
.
- في مجموعة المصدر test، انقر بزر الماوس الأيمن على New -> Package.
- أنشئ حزمة data بحيث تحتوي على حزمة source.
- أنشئ صفًا جديدًا باسم
FakeDataSource
في حزمة data/source.
الخطوة 2: تنفيذ واجهة مهام DataData
لكي تتمكّن من استخدام الصف الجديد 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 من عدم تنفيذ الطرق المطلوبة للتطبيق TasksDataSource
.
- استخدم قائمة الإصلاح السريع واختر تنفيذ الأعضاء.
- اختَر جميع الطرق واضغط على حسنًا.
الخطوة 3: تنفيذ طريقة getTasks في FakeDataSource
FakeDataSource
هو نوع معيّن من الاختبارات يُسمّى تزييف. المزيف هو اختبار اختبار متطور يتم تطبيقه على الصف، فضلاً عن تنفيذ دروس الصف، ولكن يتم تطبيقه بطريقة تجعله مناسبًا للاختبارات ولكنه غير مناسب للإنتاج. &&; العمل واقتباس; يعني أن الصف سيقدم مخرجات واقعية بناءً على المدخلات.
على سبيل المثال، لن يتصل مصدر البيانات المزيف التابع لك بالشبكة أو يحفظ أي شيء في قاعدة بيانات، بل سيستخدم فقط قائمة في الذاكرة. سيعمل ذلك على النحو المتوقّع المنطوق على العملية باستخدام هذه الطرق للحصول على المهام أو حفظها.
FakeDataSource
- يتيح لك اختبار الرمز في
DefaultTasksRepository
بدون الحاجة إلى الاعتماد على قاعدة بيانات أو شبكة حقيقية. - ويوفّر تنفيذاً حقيقيًا&قدرًا كافيًا من الاختبارات للاختبارات.
- يمكنك تغيير طريقة إنشاء
FakeDataSource
لإنشاءvar
باسمtasks
وهوMutableList<Task>?
مع قيمة تلقائية لقائمة متغيّرات فارغة.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
هذه هي قائمة المهام التي &&كثيرة&كثيرة، والتي تمثل قاعدة بيانات أو استجابة الخادم. والهدف الحالي هو اختبار طريقة repository's getTasks
. ويؤدي ذلك إلى استدعاء الأساليب source data's getTasks
وdeleteAllTasks
وsaveTask
.
كتابة نسخة مزيفة من الطرق التالية:
- اكتب
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() Test
حان الوقت لكتابة اختبار 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: إضافة RunBlockTest
يُتوقَّع حدوث خطأ كورونين لأن getTasks
هي دالة suspend
وعليك تشغيل كوروتين لاستدعاءه. لِتَنْفِيذْ هَذَا الْإِجْرَاءْ، يَجِبُ تَوْفِيرْ مِنْطَقَة كُرْيُوتِينْ. لحل هذا الخطأ، ستحتاج إلى إضافة بعض تبعيات الدرجات لمعالجة الكوروتينات في اختباراتك.
- أضِف التبعيات المطلوبة لاختبار الكوروتينات إلى مصدر الاختبار الذي تم ضبطه باستخدام
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
لا تنسَ المزامنة!
kotlinx-coroutines-test
هي مكتبة اختبار الكوروتين، وهي مخصّصة خصيصًا لاختبار الكوروتين. لإجراء اختباراتك، استخدِم الوظيفة runBlockingTest
. وهذه دالة توفّرها مكتبة اختبار الكوروتينات. وتأخذ هذه المجموعة جزءًا من الرمز، ثم تشغّل هذه المجموعة من الرموز في سياق كوروتين خاص يعمل بشكل متزامن وعلى الفور، ما يعني أنه سيتم تنفيذ الإجراءات بترتيب حاسم. ويؤدّي ذلك إلى عمل الكوروتينات مثل الكوروتين، لذا فهو مخصّص لاختبار الرموز.
استخدام runBlockingTest
في صفوف الاختبار عند استدعاء دالة suspend
. ستتعرّف على المزيد من المعلومات عن آلية عمل runBlockingTest
وكيفية اختبار الكوروتينات في الدرس التطبيقي التالي حول الترميز في هذه السلسلة.
- أضِف
@ExperimentalCoroutinesApi
أعلى الصف. ويوضح ذلك أنك تعرف أنك تستخدم واجهة برمجة تطبيقات للإصدارات التجريبية من الكوروتين (runBlockingTest
) في الصف الدراسي. وبدون ذلك، ستتلقى تحذيرًا. - ارجع إلى
DefaultTasksRepositoryTest
، وأضِفrunBlockingTest
حتى يخضع للاختبار بالكامل كـ "block"
يبدو هذا الاختبار النهائي على أنه الرمز الوارد أدناه.
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}
- نفِّذ اختبار
getTasks_requestsAllTasksFromRemoteDataSource
الجديد وتأكَّد من أنه يعمل وفقًا للخطأ.
لقد اطّلعت على كيفية اختبار وحدة في مستودع. في هذه الخطوات التالية، ستستخدم مرة أخرى إدخال التبعية وإنشاء اختبار آخر مرتين، ولكن هذه المرة لعرض كيفية كتابة اختبارات الوحدات والدمج لنماذج طرق العرض.
يجب أن تختبر اختبارات الوحدة فقط الفئة أو الطريقة التي تهتم بها. ويُعرف ذلك باسم الاختبار في ميزة عزل، حيث يمكنك عزل &"unit" بشكل واضح واختبار الرمز الذي يُعد جزءًا من هذه الوحدة فقط.
لذا، على TasksViewModelTest
اختبار رمز TasksViewModel
فقط، ولا يجب اختباره في قاعدة البيانات أو الشبكة أو فئات المستودع. وبالتالي، بالنسبة إلى نماذج الملفات الشخصية، مثلما فعلت للتو في المستودع، ستنشئ مستودعًا مزيفًا وستطبّق حقن الاعتمادية لاستخدامه في اختباراتك.
في هذه المهمة، يمكنك تطبيق إدخال التبعية لعرض النماذج.
الخطوة الأولى: إنشاء واجهة "مهام Google"
تتلخص الخطوة الأولى نحو استخدام طريقة تعتمد على التركيبات البرمجية في إنشاء واجهة مشتركة بين المحتوى المزيف والصف الحقيقي.
كيف يبدو هذا الأمر عمليًا؟ انظر إلى TasksRemoteDataSource
وTasksLocalDataSource
وFakeDataSource
، ولاحظوا أن جميعهم يشتركون في الواجهة نفسها: TasksDataSource
. وهذا يسمح لك بأن تقول في طريقة إنشاء DefaultTasksRepository
أنك ستأخذ TasksDataSource
.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
وهي تتيح لنا تبديل جهازك في FakeDataSource
.
بعد ذلك، يمكنك إنشاء واجهة لتطبيق DefaultTasksRepository
، مثلما فعلت مع مصادر البيانات. يلزم تضمين جميع الطرق العامة (سطح واجهة برمجة التطبيقات العامة) لـ DefaultTasksRepository
.
- افتَح
DefaultTasksRepository
وانقر بزر الماوس الأيمن على اسم الصف. بعد ذلك، اختَر ReACTOR -> استخراج -> واجهة.
- اختَر استخراج ملفات منفصلة.
- في نافذة استخراج الواجهة، غيّر اسم الواجهة إلى
TasksRepository
. - في القسم واجهة أعضاء النموذج، تحقَّق من جميع الأعضاء باستثناء العضوَين المصاحبَين والطُرق الخاصة الخاصة.
- انقر على إعادة بناء. يجب أن تظهر واجهة
TasksRepository
الجديدة في حزمة البيانات/المصدر.
ويطبّق DefaultTasksRepository
الآن TasksRepository
.
- شغِّل تطبيقك (وليس الاختبارات) للتأكد من أن كل شيء لا يزال صالحًا للعمل.
الخطوة الثانية: إنشاء FakeTestRepository
الآن بعد إنشاء الواجهة، يمكنك إنشاء اختبار DefaultTaskRepository
مرتين.
- في المجموعة المصدر test، في data/source، أنشئ ملف Kotlin والفئة
FakeTestRepository.kt
وتمتد من واجهةTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
سيُطلب منك تنفيذ طرق الواجهة.
- مرِّر مؤشر الماوس فوق الخطأ حتى تظهر لك قائمة الاقتراحات، ثم انقر على تنفيذ الأعضاء.
- اختَر جميع الطرق واضغط على حسنًا.
الخطوة الثالثة. تطبيق طرق 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
}
الخطوة الرابعة: إضافة طريقة للاختبار إلى 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
's.
تختلف هذه العملية قليلاً مع نماذج العرض لأنك لا تنشئها مباشرةً. على سبيل المثال:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
كما هو الحال في الرمز أعلاه، أنت تستخدم تفويض الموقع في viewModel's
الذي ينشئ نموذج الملف الشخصي. لتغيير كيفية إنشاء نموذج العرض، ستحتاج إلى إضافة ViewModelProvider.Factory
واستخدامه. إذا لم تكن على دراية بـ ViewModelProvider.Factory
، يمكنك معرفة المزيد من المعلومات هنا.
الخطوة الأولى: إنشاء وعرض_نموذج في "مهام مهام العرض"
ستبدأ بتعديل الصفوف والاختبار المرتبط بشاشة 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))
}
- يمكنك تشغيل رمز تطبيقك والتأكد من أن كل شيء لا يزال على ما يرام.
الخطوة الثانية: استخدام FakeTestRepository داخل "مهام"
وبدلاً من استخدام المستودع الفعلي في اختبارات نماذج العرض، يمكنك استخدام المستودع المزيف.
- افتح
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)
}
- يمكنك أيضًا إزالة التعليق التوضيحي
@RunWith(AndroidJUnit4::class)
لأنه لم تعد تستخدم رمز اختبار AndroidXApplicationProvider.getApplicationContext
. - احرص على إجراء الاختبارات وتأكّد من أنّها ما زالت تعمل.
من خلال استخدام حقن الاعتمادات الإنشائية، تكون قد أزلت DefaultTasksRepository
بصفتها تبعية واستبِدلتها بـ FakeTestRepository
في الاختبارات.
الخطوة الثالثة. تعديل أيضًا تجزئة مهام ونموذج العرض
أدخِل التغييرات نفسها على TaskDetailFragment
وTaskDetailViewModel
. وسيؤدي هذا الإجراء إلى إعداد الرمز عند كتابة اختبارات TaskDetail
التالية.
- فتح
TaskDetailViewModel
: - حدِّث طريقة الإنشاء:
TaskDetailsViewmodel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
- أضِف
TaskDetailViewModelFactory
في أسفل الملفTaskDetailViewModel
خارج الصف.
TaskDetailsViewmodel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}
- يجب تحديث
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
- مكتبات الإسبريسو والموكيتو
تختبر اختبارات الدمج تفاعل العديد من الصفوف الدراسية للتأكُّد من أنها تعمل بالشكل المتوقع عند استخدامها معًا. يمكن إجراء هذه الاختبارات إما محليًا (test
مجموعة مصادر) أو كاختبارات قياس قياس (androidTest
مجموعة مصادر).
في مثل هذه الحالة، ستجري كل عملية اختبار ودمج نماذج الدمج للأجزاء ونموذج العرض لاختبار الميزات الرئيسية للجزء.
الخطوة الأولى: إضافة تبعيات 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
لإضافتها إلى تبعياتها.
الخطوة الثانية: إنشاء دورة تدريبية في TaskDetailsFragmentTest
يعرض TaskDetailFragment
معلومات حول مهمة واحدة.
ستبدأ بكتابة اختبار اختبار لـ TaskDetailFragment
بما أنه يحتوي على وظائف أساسية إلى حد ما مقارنةً بالأجزاء الأخرى.
- فتح
taskdetail.TaskDetailFragment
: - أنشئ اختبارًا للنطاق
TaskDetailFragment
، على النحو الذي فعلته من قبل. اقبل الاختيارات التلقائية وضعها في مجموعة مصادر androidTest (وليس في مجموعة مصادرtest
).
- أضِف التعليقات التوضيحية التالية إلى الصف
TaskDetailFragmentTest
.
TaskDetailsFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
والغرض من هذا التعليق التوضيحي هو:
@MediumTest
: للإشارة إلى الاختبار باعتباره "&&مقاسة وقت التشغيل المتوسط" (مقارنةً باختبارات الوحدة@SmallTest
واختبارات@LargeTest
الكبيرة). ويساعدك ذلك على تجميع الاختبار واختياره.@RunWith(AndroidJUnit4::class)
: يُستخدم هذا الإعداد في أي صف باستخدام اختبار AndroidX.
الخطوة الثالثة. تشغيل جزء من اختبار
في هذه المهمة، ستطلق TaskDetailFragment
باستخدام مكتبة اختبارات AndroidX. FragmentScenario
هي فئة من اختبار AndroidX يتم تضمينها حول جزء من الاختبار وتمنحك إمكانية التحكُّم المباشر في الجزء المتغيّر من الاختبار. لكتابة اختبارات للأجزاء، عليك إنشاء FragmentScenario
للجزء الذي تختبره (TaskDetailFragment
).
- نسخ هذا الاختبار إلى
TaskDetailFragmentTest
TaskDetailsFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
الرمز التالي:
- إنشاء مهمة.
- تنشئ
Bundle
، والتي تمثل وسيطات الأجزاء للمهمة التي يتم تمريرها إلى الجزء. - تنشئ الدالة
launchFragmentInContainer
FragmentScenario
، باستخدام هذه الحزمة ومظهر.
لم تكتمل هذه التجربة بعد، لأنها لا تؤكد أي شيء. في الوقت الحالي، يمكنك إجراء الاختبار وملاحظة ما يحدث.
- هذا اختبار يتم قياسه، لذا تأكّد من ظهور المحاكي أو جهازك.
- إجراء الاختبار.
يجب إجراء بعض الأمور.
- أولاً، لأن هذا اختبار تم قياسه، سيتم إجراء الاختبار إما على جهازك الفعلي (إذا كان متصلاً) أو محاكيًا.
- ومن المفترَض أن يؤدي ذلك إلى إطلاق الكسر.
- لاحِظ كيف لا يتنقّل العنصر في أي جزء آخر أو لا يحتوي على أي قوائم مرتبطة بالنشاط، بل فقط جزء منه.
وأخيرًا، انظر بإمعان ولاحظ أن الجزء يقول "&data;No data" حيث إنه لا يحمّل بيانات المهمة بنجاح.
يجب أن يحمّل الاختبار كلاً من TaskDetailFragment
(لقد أجريت ذلك) وتأكيد تحميل البيانات بشكلٍ صحيح. لماذا لا تتوفّر أي بيانات؟ هذا لأنك أنشأت مهمة، ولكنك لم تحفظها في المستودع.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
لديك هذا FakeTestRepository
، ولكنك تحتاج إلى طريقة لاستبدال مستودعك الفعلي بمستودعك المزيّف للجزء. الخطوات التالية
في هذه المَهمة، عليك تقديم مستودعك الزائف إلى الجزء المستخدَم باستخدام ServiceLocator
. وسيسمح لك هذا بكتابة اختبارات أجزاء من أجزاء الجمهور وعرضها.
لا يمكنك استخدام إدخال تبعية البناء هنا، كما كنت تفعل من قبل، عندما تحتاج إلى الاعتماد على نموذج الملف الشخصي أو المستودع. يتطلّب إدخال تبعية المُنشئ إنشاء الفئة. التجزئة والأنشطة هي أمثلة للصفوف التي لا تنشئها ولا تستخدمها بشكل عام.
بما أنّه لا يمكنك إنشاء الجزء، لا يمكنك استخدام إدخال تبعية الدالة الإنشائية لتبديل اختبار المستودع مرتين (FakeTestRepository
) إلى الجزء. بدلاً من ذلك، استخدِم النمط محدِّد خدمة. يُعدّ نمط "محدِّد الخدمة" بديلاً عن حقن التبعية. ويشمل ذلك إنشاء فئة مفردة تُسمى "&&حاصل تحديد موقع الخدمة" والغرض منه هو توفير تبعيات لكلٍّ من الرمز العادي والاختبار. في رمز التطبيق العادي (مجموعة المصدر main
)، تكون كل الاعتماديات هذه هي ارتباطات تطبيقات عادية. بالنسبة إلى الاختبارات، يمكنك تعديل محدِّد مواقع الخدمات لتقديم إصدارات مزدوجة من التبعيات.
عدم استخدام محدِّد مواقع الخدمات | استخدام محدِّد مواقع الخدمات |
ويجب تنفيذ ما يلي في هذا التطبيق التطبيقي حول الترميز:
- أنشئ صفًا لمحدد الخدمة الذي يمكنه إنشاء مستودع وتخزينه. وتُنشئ هذه الأداة تلقائيًا مستودعًا &&عرضًا عاديًا.
- أعد إنشاء الرمز بحيث يمكنك استخدام "محدِّد الخدمة" عند الحاجة إلى مستودع.
- في صف الاختبار، اتّبع طريقة على "محدِّد الخدمة" تعمل على استبدال "normal;quot; المستودع الاختباري المزدوج.
الخطوة الأولى: إنشاء معرّف الخدمة
لنجعل صف 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
}
}
الخطوة الثانية: استخدام 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
}
}
}
}
في كل مكان كنت تستخدم فيه getRepository
، يمكنك استخدام التطبيق taskRepository
بدلاً من ذلك. وهذا يضمن أنه بدلاً من إنشاء المستودع مباشرةً، ستحصل على أي مستودع مقدّم من ServiceLocator
.
- افتح
TaskDetailFragement
وابحث عن المكالمة التي تم إرسالها إلىgetRepository
في أعلى الصف. - استبدل هذه المكالمة بمكالمة تحصل على المستودع من
TodoApplication
.
TaskDetailsFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
- يمكنك تنفيذ الإجراء نفسه مع
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
- شغِّل تطبيقك (وليس الاختبار)!
وبما أنّك قد أجريت عملية إعادة التركيب فقط، يجب تشغيل التطبيق نفسه بدون مشكلة.
الخطوة الثالثة. إنشاء FakeAndroidTestRepository
لديك FakeTestRepository
حاليًا في مجموعة مصادر الاختبار. لا يمكنك مشاركة فئات الاختبار بين مجموعات مصادر test
وandroidTest
بشكل تلقائي. لذلك، عليك إنشاء صف FakeTestRepository
مكرر في مجموعة المصدر androidTest
، واستدعاءه FakeAndroidTestRepository
.
- انقر بزر الماوس الأيمن على مجموعة مصادر
androidTest
وأنشئ حزمة data. انقر بزر الماوس الأيمن مرة أخرى وأنشئ حزمة مصدر. - أنشئ صفًا جديدًا في حزمة المصدر هذه باسم
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() }
}
}
الخطوة الرابعة: تحضير مُحدِّد الخدمة للاختبارات
تَمَامْ، دَهْ وَقْتِ اسْتِخْدَامْ ServiceLocator
لِبَدْلِ الْمُكَالْمَة فِي الْفَتْحَة. لإجراء ذلك، عليك إضافة رمز إلى رمز ServiceLocator
.
- فتح
ServiceLocator.kt
: - ضَع علامة على اسم
tasksRepository
باعتباره@VisibleForTesting
. يُعد هذا التعليق التوضيحي طريقة للتعبير عن السبب وراء ضبط التعيين على "عام" بسبب الاختبار.
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
}
}
الخطوة الخامسة. استخدام معرّف الخدمة
في هذه الخطوة، يمكنك استخدام ServiceLocator
.
- فتح
TaskDetailFragmentTest
: - حدِّد متغيّر
lateinit TasksRepository
. - أضِف إعدادًا وطريقة لإزالة
FakeAndroidTestRepository
لإعداده قبل كل اختبار وتنظيفه بعد كل اختبار.
TaskDetailsFragmentTest.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)
يبدو الاختبار النهائي على النحو التالي:
TaskDetailsFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
- يمكنك إضافة تعليقات توضيحية إلى الصف بأكمله باستخدام
@ExperimentalCoroutinesApi
.
عند الانتهاء، سيظهر الرمز على النحو التالي.
TaskDetailsFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- إجراء اختبار
activeTaskDetails_DisplayedInUi()
من المفترض أن ترى هذا الجزء، كما هو الحال في السابق، نظرًا لأنك أعددت المستودع بشكل صحيح، فإنه يعرض الآن معلومات المهمة.
في هذه الخطوة، ستستخدم مكتبة اختبار واجهة مستخدم Espresso لإكمال اختبار الدمج الأول. يمكنك تنظيم بنية الرمز بحيث يمكنك إضافة اختبارات بتأكيدات على واجهة المستخدم. لإجراء ذلك، عليك استخدام مكتبة اختبار الإسبريسو.
يساعدك الإسبريسو على:
- التفاعل مع طرق العرض، مثل النقر على الأزرار أو التمرير على شريط أو التمرير إلى أسفل الشاشة
- تأكّد من ظهور طرق عرض معيّنة على الشاشة أو أنها في حالة معيّنة (مثل تضمين نص معيّن أو وضع علامة في مربّع اختيار، وما إلى ذلك).
الخطوة الأولى: ملاحظة اعتماد الاعتماد على Gradle
سيكون لديك تبعية الإسبرسو الرئيسية بالفعل لأنه يتم تضمينها في مشاريع Android بشكل تلقائي.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
: يتم تضمين هذه التبعية الأساسية لـ Espresso تلقائيًا عند إنشاء مشروع جديد على Android. وهو يحتوي على رمز الاختبار الأساسي لمعظم المشاهدات والإجراءات عليها.
الخطوة الثانية: إيقاف الصور المتحركة
يتم إجراء اختبارات الإسبريسو على جهاز حقيقي، وبالتالي يتم إجراء اختبارات قياس حالة التطبيق. تتمثل إحدى المشاكل التي تظهر في الصور المتحركة: إذا تأخّرت الصورة المتحركة وحاولت اختبار ما إذا كانت المشاهدة على الشاشة ولكنّها لا تزال متحركة، يمكن أن يؤدي استخدام Espresso إلى إخفاق الاختبار عن طريق الخطأ. قد يؤدي هذا إلى جعل اختبارات الإسبريسو غير مستقرة.
بالنسبة إلى اختبار واجهة مستخدم Espresso، من أفضل الممارسات إيقاف الصور المتحركة (سيتم أيضًا إجراء الاختبار بشكل أسرع):
- على جهاز الاختبار، انتقِل إلى الإعدادات >؛ خيارات المطوّرين.
- أوقِف هذه الإعدادات الثلاثة: مقياس الرسوم المتحركة للنافذة ومقياس الصور المتحركة للنقل ومقياس مدة الصور المتحركة.
الخطوة الثالثة. انظر إلى اختبار الإسبريسو.
قبل كتابة اختبار الإسبريسو، ألقِ نظرة على بعض رموز الإسبريسو.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
ويتمثّل هذا البيان في البحث عن طريقة عرض مربّع الاختيار التي تحمل رقم التعريف task_detail_complete_checkbox
والنقر عليه ثم تأكيده.
تتألف غالبية كشوف الإسبريسو من أربعة أجزاء:
onView
onView
هي مثال على طريقة استخدام إسبريسو ثابتة لبدء بيان الإسبرسو. وتُعد onView
من أكثر الخيارات شيوعًا، ولكن هناك خيارات أخرى، مثل onData
.
2. ViewViewer
withId(R.id.task_detail_title_text)
withId
هي مثال على ViewMatcher
تحصل على مشاهدة حسب رقم تعريفها. هناك أدوات مطابقة أخرى للملف الشخصي يمكنك البحث عنها في المستندات.
3- ViewAction
perform(click())
طريقة perform
التي تستخدم ViewAction
. ViewAction
هو إجراء يمكن تنفيذه في الملف الشخصي، على سبيل المثال، النقر على الملف الشخصي.
4. عرض التأكيد
check(matches(isChecked()))
check
والتي تستغرق ViewAssertion
. تتحقّق ViewAssertion
من صحة الصورة أو تؤكّدها. يشكّل تأكيد matches
الأكثر استخدامًا ViewAssertion
. لإنهاء عملية التأكيد، استخدِم ViewMatcher
آخر، في هذه الحالة isChecked
.
يُرجى العلم بأنك لا تتصل دائمًا بكل من perform
وcheck
في بيان إسبرسو. يمكنك الحصول على كشوفات للتأكيد فقط باستخدام check
أو ViewAction
فقط باستخدام perform
.
- فتح
TaskDetailFragmentTest.kt
: - عدِّل اختبار
activeTaskDetails_DisplayedInUi
.
TaskDetailsFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
فيما يلي كشوفات الاستيراد، إذا لزم الأمر:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
- كل الميزات التي تلي التعليق على
// THEN
تستخدم الإسبريسو. تحقّق من بنية الاختبار واستخدامwithId
وتحقّق من كيفية ظهور صفحة التفاصيل. - أجرِ الاختبار وأكِّد نجاحه.
الخطوة الرابعة: كتابة اختبار الإسبريسو الخاص بك اختياريًا
والآن اكتب اختبارًا بنفسك.
- أنشِئ اختبارًا جديدًا باسم
completedTaskDetails_DisplayedInUi
وانسخ رمز الهيكل العظمي هذا.
TaskDetailsFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}
- بعد الاطّلاع على الاختبار السابق، يمكنك إكمال هذا الاختبار.
- نفِّذ وأكِّد نجاح الاختبارات.
يجب أن يظهر completedTaskDetails_DisplayedInUi
الانتهاء على النحو التالي لهذا الرمز.
TaskDetailsFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}
في هذه الخطوة الأخيرة، ستتعرّف على كيفية اختبار مكوّن التنقّل، باستخدام نوع مختلف من الاختبارات ثنائية يُطلق عليها اسم وهمية ومكتبة الاختبار Mockito.
في هذا الدرس التطبيقي حول الترميز، استخدمت اختبارًا مزدوجًا يُعرف باسم مزيف. تُعدّ البرامج المزيفة أحد أنواع العديد من أنواع اختبار الضعف. ما الاختبار المزدوج الذي يجب استخدامه لاختبار مكوّن التنقل؟
فكِّر في كيفية التنقل. تخيَّل أنك ضغطت على إحدى المهام في TasksFragment
للانتقال إلى شاشة تفاصيل المهمة.
رمز هنا في TasksFragment
ينتقل إلى شاشة تفاصيل المهمة عند الضغط عليها.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
يحدث التنقّل بسبب استدعاء الإجراء navigate
. إذا كنت بحاجة إلى كتابة بيان تأكيد، لا تتوفّر طريقة سهلة لاختبار ما إذا كنت قد انتقلت إلى TaskDetailFragment
. التنقّل هو إجراء معقّد لا يؤدي إلى نتيجة واضحة أو تغيير في الحالة، بخلاف إعداد TaskDetailFragment
.
ما يمكنك تأكيده هو أنّه تم استدعاء طريقة navigate
باستخدام معلّمة الإجراء الصحيحة. وهذا هو بالضبط ما يقوم به اختبار اختباري مرتين، وهو التحقق مما إذا تم استدعاء طرق محددة أم لا.
Mockito هو إطار عمل لإنشاء اختبارات مزدوجة. على الرغم من استخدام كلمة وهمية في واجهة برمجة التطبيقات واسمها، إلا أنّها لا تُصنع نماذج زائفة. ويمكن أيضًا إعداد التنويهات الموجزة والتوابل.
ستستخدم Mockito لإنشاء وهمية NavigationController
يمكنها تأكيد أن طريقة التنقل تم استدعاؤها بشكل صحيح.
الخطوة الأولى: إضافة تبعيات Gradle
- أضِف تبعيات الدرجات.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core
—هذه هي اعتمادية موكيتو.dexmaker-mockito
: هذه المكتبة مطلوبة لاستخدام Mockito في مشروع متوافق مع Android. تحتاج Mockito إلى إنشاء صفوف في وقت التشغيل. على نظام التشغيل Android، يتم إجراء ذلك باستخدام رمز dex بايت، وبالتالي تسمح هذه المكتبة لتطبيق Mockito بإنشاء العناصر أثناء وقت التشغيل على Android.androidx.test.espresso:espresso-contrib
: تتألف هذه المكتبة من مساهمات خارجية (باسمها) تحتوي على رمز اختبار لمشاهدات أكثر تقدمًا، مثلDatePicker
وRecyclerView
. وتتضمّن أيضًا عمليات تحقّق من تسهيل الاستخدام وفئة باسمCountingIdlingResource
يتم تناولها لاحقًا.
الخطوة الثانية: إنشاء مهامFFmentmentTest
- فتح
TasksFragment
- انقر بزر الماوس الأيمن على اسم الفئة في
TasksFragment
واختَر إنشاء ثم اختبار. أنشئ اختبارًا في مجموعة مصادر 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
وتتيح لك تنفيذ إجراءات الإسبريسو على RecyclerView.
- تحقَّق من أنه تم استدعاء الدالة
navigate
مع الوسيطة الصحيحة.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
طريقة Mockito's هي ما يجعل هذه العملية وهمية، وأنت قادر على تأكيد الرمز navController
الذي تم محاكاةه باستخدام طريقة معيّنة (navigate
) باستخدام إحدى المعلَمات (actionTasksFragmentToTaskDetailFragment
باستخدام معرّف "&معرّف_معرّف_التعريف_1&").
ويبدو الاختبار الكامل على النحو التالي:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}
- إجراء الاختبار
وباختصار، لاختبار التنقل، يمكنك:
- استخدم Mockito لإنشاء نموذج وهمي
NavController
. - أرفِق ذلك بالوهم
NavController
إلى الجزء. - تحقّق من أنه تم استدعاء التنقّل باستخدام الإجراء والمعلّمات الصحيحة.
الخطوة الثالثة. اختياري، اكتب clickAddTaskButton_NavigationToAddEditFragment
ولمعرفة ما إذا كان بإمكانك كتابة اختبار تنقّل بنفسك، جرِّب هذه المهمة.
- اكتب الاختبار
clickAddTaskButton_navigateToAddEditFragment
الذي يتحقّق من أنك إذا نقرت على زر الإجراء + FAB، سيتم الانتقال إلىAddEditTaskFragment
.
الإجابة أدناه.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}
انقر هنا لمعرفة الفرق بين الرمز الذي بدأته والرمز النهائي.
لتنزيل رمز الدرس التطبيقي للترميز، يمكنك استخدام الأمر git أدناه:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
بدلاً من ذلك، يمكنك تنزيل المستودع كملف Zip وفك ضغطه وفتحه في"استوديو Android".
يتناول هذا الدرس التطبيقي كيفية إعداد إدخال التبعية اليدوية ومُحدِّد مواقع الخدمات وكيفية استخدام العناصر المزيفة والاصطناعية في تطبيقات Android Kotlin. وعلى وجه الخصوص:
- ما تريد اختباره واستراتيجية اختبارك تحدّد أنواع الاختبارات التي ستنفّذها لتطبيقك. يتم التركيز على اختبارات الوحدة بشكلٍ سريع. تعمل اختبارات الدمج على التحقق من التفاعل بين أجزاء برنامجك. تعمل الاختبارات المتقدمة على التحقق من الميزات وأعلى دقة، وغالبًا ما يتم استخدام هذه الأدوات وقد يستغرق تشغيلها وقتًا أطول.
- تؤثر بنية تطبيقك في مدى صعوبة اختباره.
- TDD أو التطوير المستند إلى الاختبار هو استراتيجية تكتب فيها الاختبارات أولاً، ثم تنشئ الميزة لاجتياز الاختبارات.
- لعزل أجزاء من تطبيقك لأغراض الاختبار، يمكنك استخدام أدوات الاختبار المزدوجة. الاختبار المزدوج هو إصدار من صف تم تصميمه خصيصًا للاختبار. على سبيل المثال، يمكنك تزييف الحصول على البيانات من قاعدة بيانات أو إنترنت.
- استخدِم إدخال التبعية لاستبدال فئة فعلية بفئة اختبار، مثل مستودع أو طبقة شبكة.
- استخدِم الاختبار المضمّن (
androidTest
) لتشغيل مكوّنات واجهة المستخدم. - عندما يتعذّر عليك استخدام إدخال تبعية لدالة الإنشاء، على سبيل المثال لإطلاق جزء، يمكنك في كثير من الأحيان استخدام محدِّد مواقع الخدمات. يُعدّ نمط محدِّد مواقع الخدمات بديلاً عن إدخال التبعية. ويشمل ذلك إنشاء فئة مفردة تُسمى "&&حاصل تحديد موقع الخدمة" والغرض منه هو توفير تبعيات لكلٍّ من الرمز العادي والاختبار.
دورة Udacity:
مستندات مطوّر برامج Android:
- دليل بنية التطبيقات
runBlocking
وrunBlockingTest
FragmentScenario
- الإسبريسو
- الموكيتو
- JUnit4
- مكتبة اختبارات AndroidX
- مكتبة الاختبارات الأساسية لمكونات AndroidX
- مجموعات المصادر
- الاختبار من سطر الأوامر
فيديوهات:
غير ذلك:
للحصول على روابط إلى دروس تطبيقية أخرى حول الترميز في هذه الدورة التدريبية، اطّلِع على الصفحة المقصودة للإصدارات المتقدّمة من Android في لغة ترميز Kotlin.