هذه المقالة هي الأخيرة في سلسلة حول تطبيق النمط المعماري MVI في Kotlin Multiplatform. في شطري السابقة ( جزء 1 و جزء 2 )، ونحن نتذكر ما هو MVI، وخلق القطط العامة وحدة لتحميل الصور القط ودمجها في التطبيقات دائرة الرقابة الداخلية والروبوت.
في هذا الجزء ، سنغطي وحدة Kittens باختبارات الوحدة والتكامل. سوف نتعرف على القيود الحالية للاختبار في Kotlin Multiplatform ، وسنتعرف على كيفية التغلب عليها وحتى نجعلها تعمل لصالحنا.
يتوفر نموذج مشروع محدث على GitHub .
مقدمة
ليس هناك شك في أن الاختبار هو خطوة مهمة في تطوير البرمجيات. بالطبع ، يبطئ العملية ، لكن في نفس الوقت:
- يسمح لك بالتحقق من حالات الحافة التي يصعب التقاطها يدويًا ؛
- يقلل من فرصة الانحدار عند إضافة ميزات جديدة وإصلاح الأخطاء وإعادة البناء ؛
- يجبرك على تفكيك وتكوين الكود الخاص بك.
للوهلة الأولى ، قد تبدو النقطة الأخيرة عيبًا ، لأنها تستغرق وقتًا. ومع ذلك ، فإنه يجعل الكود أكثر قابلية للقراءة وفائدة على المدى الطويل.
"في الواقع ، فإن نسبة الوقت المستغرق في القراءة مقابل الكتابة تزيد عن 10 إلى 1. نحن نقرأ باستمرار الرموز القديمة كجزء من الجهود المبذولة لكتابة رمز جديد. ... [لذلك] جعلها سهلة القراءة تجعل الكتابة أسهل. " - روبرت سي مارتن ، "الكود النظيف: دليل لبرمجيات أجايل"
توسع Kotlin Multiplatform قدرات الاختبار. تضيف هذه التقنية ميزة مهمة واحدة: يتم إجراء كل اختبار تلقائيًا على جميع الأنظمة الأساسية المدعومة. على سبيل المثال ، إذا تم دعم Android و iOS فقط ، فيمكن مضاعفة عدد الاختبارات باثنين. وإذا تمت إضافة دعم لمنصة أخرى في وقت ما ، فسيتم تغطيته تلقائيًا في الاختبارات.
يعد الاختبار على جميع الأنظمة الأساسية المدعومة أمرًا مهمًا لأنه قد تكون هناك اختلافات في سلوك الكود. على سبيل المثال ، يحتوي Kotlin / Native على نموذج ذاكرة خاص ، كما يعطي Kotlin / JS أحيانًا نتائج غير متوقعة.
قبل أن نذهب إلى أبعد من ذلك ، تجدر الإشارة إلى بعض قيود الاختبار في Kotlin Multiplatform. أكبرها هو عدم وجود أي مكتبة ساخرة لـ Kotlin / Native و Kotlin / JS. قد يبدو هذا عيبًا كبيرًا ، لكنني شخصياً أعتبره ميزة. كان الاختبار في Kotlin Multiplatform صعبًا للغاية بالنسبة لي: كان علي إنشاء واجهات لكل تبعية وكتابة تطبيقات الاختبار الخاصة بهم (مزيفة). استغرق الأمر وقتًا طويلاً ، لكن في مرحلة ما أدركت أن قضاء الوقت في التجريد هو استثمار يؤدي إلى كود أنظف.
لقد لاحظت أيضًا أن التعديلات اللاحقة على هذا الرمز تستغرق وقتًا أقل. لماذا هذا؟ لأن تفاعل الطبقة مع تبعياتها لا يتم تسميته (سخرية). في معظم الحالات ، يكفي تحديث تطبيقات الاختبار الخاصة بهم. ليست هناك حاجة للتعمق في كل طريقة اختبار لتحديث النماذج. نتيجة لذلك ، توقفت عن استخدام المكتبات الساخرة حتى في تطوير Android القياسي. أوصي بقراءة المقال التالي: " السخرية ليست عملية - استخدم المنتجات المقلدة " برافين سوناوان .
خطة
دعونا نتذكر ما لدينا في وحدة Kittens وما يجب اختباره.
- KittenStore هو المكون الرئيسي للوحدة. يحتوي تطبيق KittenStoreImpl على معظم منطق الأعمال. هذا هو أول شيء سنختبره.
- KittenComponent هي واجهة الوحدة النمطية ونقطة التكامل لجميع المكونات الداخلية. سنغطي هذا المكون باختبارات التكامل.
- KittenView هي واجهة عامة تمثل تبعية واجهة المستخدم لـ KittenComponent.
- KittenDataSource هي واجهة وصول داخلية للويب لها تطبيقات خاصة بالنظام الأساسي لنظامي التشغيل iOS و Android.
من أجل فهم أفضل لهيكل الوحدة ، سأقدم مخطط UML الخاص بها:
الخطة كما يلي:
- اختبار KittenStore
- إنشاء تطبيق تجريبي لـ KittenStore.Parser
- إنشاء تطبيق تجريبي لـ KittenStore.Network
- اختبارات وحدة الكتابة لـ KittenStoreImpl
- إنشاء تطبيق تجريبي لـ KittenStore.Parser
- اختبار KittenComponent
- إنشاء تطبيق اختباري لـ KittenDataSource
- إنشاء تطبيق اختبار KittenView
- اختبارات تكامل الكتابة لـ KittenComponent
- إنشاء تطبيق اختباري لـ KittenDataSource
- اختبارات التشغيل
- الاستنتاجات
اختبار وحدة KittenStore
تحتوي واجهة KittenStore على فئة التنفيذ الخاصة بها - KittenStoreImpl. هذا ما سنختبره. لها تبعيتان (واجهات داخلية) ، محددتان مباشرة في الفصل نفسه. لنبدأ بكتابة تطبيقات الاختبار لهم.
اختبار تنفيذ KittenStore.Parser
هذا المكون مسؤول عن طلبات الشبكة. هذا ما تبدو عليه واجهته:
قبل كتابة تنفيذ اختباري لواجهة الشبكة ، نحتاج إلى الإجابة على سؤال مهم واحد: ما هي البيانات التي يعيدها الخادم؟ الجواب هو أن الخادم يعيد مجموعة عشوائية من روابط الصور ، في كل مرة مجموعة مختلفة. في الحياة الواقعية ، يتم استخدام تنسيق JSON ، ولكن نظرًا لأن لدينا تجريد Parser ، فنحن لا نهتم بالتنسيق في اختبارات الوحدة.
يمكن للتنفيذ الحقيقي تبديل التدفقات ، بحيث يمكن تجميد المشتركين في Kotlin / Native. سيكون من الرائع تصميم هذا السلوك للتأكد من أن الكود يعالج كل شيء بشكل صحيح.
لذلك ، يجب أن يحتوي تطبيقنا الاختباري للشبكة على الميزات التالية:
- يجب إرجاع مجموعة غير فارغة من الصفوف المختلفة لكل طلب ؛
- يجب أن يكون تنسيق الاستجابة شائعًا للشبكة والمحلل اللغوي ؛
- يجب أن يكون قادرًا على محاكاة أخطاء الشبكة (ربما يجب أن يكتمل بدون استجابة) ؛
- يجب أن يكون من الممكن محاكاة تنسيق استجابة غير صالح (للتحقق من وجود أخطاء في Parser) ؛
- ينبغي أن يكون من الممكن محاكاة تأخيرات الاستجابة (للتحقق من مرحلة التمهيد) ؛
- يجب أن يكون قابلاً للتجميد في Kotlin / Native (فقط في حالة).
قد يبدو تطبيق الاختبار نفسه كما يلي:
يحتوي TestKittenStoreNetwork على مخزن خيوط (تمامًا مثل الخادم الحقيقي) ويمكنه إنشاؤه. لكل طلب ، يتم ترميز قائمة الأسطر الحالية في سطر واحد. إذا كانت خاصية "الصور" تساوي صفرًا ، فحينئذٍ سيتم إنهاء "ربما" فقط ، وهو ما يجب اعتباره خطأ.
استخدمنا أيضًا TestScheduler . هذا المجدول له وظيفة واحدة مهمة: فهو يجمد جميع المهام الواردة. وبالتالي ، فإن عامل المراقبة ، المستخدم بالاقتران مع TestScheduler ، سوف يجمد المصب ، وكذلك جميع البيانات التي تمر عبره ، تمامًا كما هو الحال في الحياة الواقعية. ولكن في الوقت نفسه ، لن يتم تضمين تعدد مؤشرات الترابط ، مما يبسط الاختبار ويجعله أكثر موثوقية.
بالإضافة إلى ذلك ، يحتوي TestScheduler على وضع "معالجة يدوية" خاص يسمح لنا بمحاكاة زمن انتقال الشبكة.
اختبار تنفيذ KittenStore.Parser
هذا المكون مسؤول عن تحليل الاستجابات من الخادم. ها هي واجهته:
لذلك ، كل ما يتم تنزيله من الويب يجب تحويله إلى قائمة روابط. تقوم شبكتنا ببساطة بتوصيل السلاسل باستخدام فاصلة منقوطة (؛) ، لذا استخدم نفس التنسيق هنا.
إليك تطبيق تجريبي:
كما هو الحال مع Network ، يتم استخدام TestScheduler لتجميد المشتركين والتحقق من توافقهم مع نموذج ذاكرة Kotlin / Native. تتم محاكاة أخطاء معالجة الاستجابة إذا كانت سلسلة الإدخال فارغة.
اختبارات الوحدة لـ KittenStoreImpl
لدينا الآن تطبيقات اختبار لجميع التبعيات. حان الوقت لاختبارات الوحدة. يمكن العثور على جميع اختبارات الوحدة في المستودع ، وهنا سأقدم فقط التهيئة وبعض الاختبارات بأنفسهم.
تتمثل الخطوة الأولى في إنشاء مثيلات لتطبيقات الاختبار الخاصة بنا:
يستخدم KittenStoreImpl mainScheduler ، لذا فإن الخطوة التالية هي تجاوزه:
الآن يمكننا إجراء بعض الاختبارات. يجب أن يقوم KittenStoreImpl بتحميل الصور فور إنشائها. هذا يعني أنه يجب إكمال طلب الشبكة ، ويجب معالجة استجابته ، ويجب تحديث الحالة بالنتيجة الجديدة.
ماذا فعلنا:
- الصور التي تم إنشاؤها على الشبكة ؛
- إنشاء مثيل جديد من KittenStoreImpl ؛
- تأكد من احتواء الدولة على قائمة السلاسل الصحيحة.
سيناريو آخر نحتاج إلى التفكير فيه هو الحصول على KittenStore.Intent.Reload. في هذه الحالة ، يجب إعادة تحميل القائمة من الشبكة.
خطوات الاختبار:
- توليد صور المصدر ؛
- إنشاء مثيل لـ KittenStoreImpl ؛
- توليد صور جديدة
- إرسال النية.
- تأكد من أن الحالة تحتوي على صور جديدة.
أخيرًا ، دعنا نختبر السيناريو التالي: عند تعيين علامة isLoading أثناء تحميل الصور.
لقد قمنا بتمكين المعالجة اليدوية لـ TestScheduler - الآن لن تتم معالجة المهام تلقائيًا. يتيح لنا هذا التحقق من الحالة أثناء انتظار الرد.
اختبار التكامل KittenComponent
كما ذكرت أعلاه ، فإن KittenComponent هي نقطة تكامل الوحدة بأكملها. يمكننا تغطيتها باختبارات التكامل. دعنا نلقي نظرة على API الخاص به:
هناك نوعان من التبعيات ، KittenDataSource و KittenView. سنحتاج إلى تطبيقات تجريبية لهذه قبل أن نتمكن من بدء الاختبار.
للتأكد من اكتمالها ، يوضح هذا الرسم البياني تدفق البيانات داخل الوحدة:
تطبيق اختبار KittenDataSource
هذا المكون مسؤول عن طلبات الشبكة. لديها تطبيقات منفصلة لكل منصة ، ونحن بحاجة إلى تطبيق آخر للاختبارات. هذا ما تبدو عليه واجهة KittenDataSource:
يدعم TheCatAPI ترقيم الصفحات ، لذلك أضفت الوسائط المناسبة على الفور. بخلاف ذلك ، فهو مشابه جدًا لـ KittenStore.Network ، الذي قمنا بتطبيقه مسبقًا. الاختلاف الوحيد هو أنه يجب علينا استخدام تنسيق JSON لأننا نختبر رمزًا حقيقيًا في التكامل. لذلك نحن فقط نستعير فكرة التنفيذ:
كما في السابق ، نقوم بإنشاء قوائم مختلفة من السلاسل المشفرة في مصفوفة JSON عند كل طلب. إذا لم يتم إنشاء أي صور ، أو إذا كانت وسيطات الطلب خاطئة ، فربما ينتهي الأمر بدون رد. تُستخدم
مكتبة kotlinx.serialization لتشكيل مصفوفة JSON . بالمناسبة ، فإن KittenStoreParser الذي تم اختباره يستخدمه لفك التشفير.
تنفيذ اختبار KittenView
هذا هو المكون الأخير الذي نحتاج إلى تنفيذ تجريبي له قبل أن نبدأ في الاختبار. ها هي واجهته:
إنها وجهة نظر تأخذ النماذج وتطلق الأحداث فقط ، لذا فإن تنفيذها التجريبي بسيط للغاية:
نحتاج فقط إلى تذكر آخر نموذج تم قبوله - سيتيح لنا ذلك التحقق من صحة النموذج المعروض. يمكننا أيضًا إرسال الأحداث نيابة عن KittenView باستخدام طريقة الإرسال (Event) ، والتي تم الإعلان عنها في فئة AbstractMviView الموروثة.
اختبارات الاندماج لـ KittenComponent
يمكن العثور على مجموعة الاختبارات الكاملة في المستودع ، وسأقدم هنا عددًا قليلاً فقط من الاختبارات الأكثر إثارة للاهتمام.
كما في السابق ، لنبدأ بإنشاء مثيل للتبعيات والتهيئة:
يوجد حاليًا جدولين مستخدمين للوحدة النمطية: mainScheduler و computationScheduler. نحن بحاجة إلى تجاوزها:
يمكننا الآن كتابة بعض الاختبارات. دعنا نتحقق من البرنامج النصي الرئيسي أولاً للتأكد من تحميل الصور وعرضها عند بدء التشغيل:
هذا الاختبار مشابه جدًا للاختبار الذي كتبناه عندما نظرنا إلى اختبارات الوحدة لـ KittenStore. الآن فقط يتم تضمين الوحدة بأكملها.
خطوات الاختبار:
- إنشاء روابط للصور في TestKittenDataSource ؛
- إنشاء وتشغيل KittenComponent ؛
- تأكد من وصول الروابط إلى TestKittenView.
سيناريو آخر مثير للاهتمام: يجب إعادة تحميل الصور عندما يطلق KittenView حدث RefreshTriggered.
مراحل:
- إنشاء روابط مصدر للصور ؛
- إنشاء وتشغيل KittenComponent ؛
- إنشاء روابط جديدة ؛
- إرسال Event.RefreshTriggered نيابة عن KittenView ؛
- تأكد من وصول الروابط الجديدة إلى TestKittenView.
اختبارات التشغيل
لإجراء جميع الاختبارات ، نحتاج إلى إجراء مهمة Gradle التالية:
./gradlew :shared:kittens:build
سيؤدي ذلك إلى تجميع الوحدة وتشغيل جميع الاختبارات على جميع الأنظمة الأساسية المدعومة: Android و iosx64.
وهنا تقرير تغطية JaCoCo:
خاتمة
في هذه المقالة ، قمنا بتغطية وحدة Kittens باختبارات الوحدة والتكامل. سمح لنا تصميم الوحدة المقترحة بتغطية الأجزاء التالية:
- KittenStoreImpl - يحتوي على معظم منطق الأعمال ؛
- KittenStoreNetwork - مسؤول عن طلبات الشبكة عالية المستوى ؛
- KittenStoreParser - مسؤول عن تحليل استجابات الشبكة ؛
- كل التحولات والوصلات.
النقطة الأخيرة مهمة للغاية. من الممكن تغطيتها بفضل ميزة MVI. مسؤولية العرض الوحيدة هي عرض البيانات وإرسال الأحداث. تتم جميع الاشتراكات والتحويلات والروابط داخل الوحدة. وبالتالي ، يمكننا تغطية كل شيء باختبارات عامة باستثناء الشاشة نفسها.
هذه الاختبارات لها المزايا التالية:
- لا تستخدم واجهات برمجة تطبيقات النظام الأساسي ؛
- أداء سريع جدا
- موثوقة (لا ترمش) ؛
- تعمل على جميع المنصات المدعومة.
تمكنا أيضًا من اختبار الكود للتوافق مع نموذج ذاكرة Kotlin / الأصلي المعقد. هذا أيضًا مهم جدًا بسبب نقص الأمان في وقت الإنشاء: يتعطل الكود في وقت التشغيل مع استثناءات يصعب تصحيحها.
أتمنى أن يساعدك هذا في مشاريعك. شكرا لقراءة مقالاتي! ولا تنسوا متابعتي على تويتر .
...
تمرين إضافي
إذا كنت ترغب في العمل مع تطبيقات الاختبار أو اللعب باستخدام MVI ، فإليك بعض التمارين العملية.
إعادة هيكلة مصدر KittenDataSource
يوجد تطبيقان لواجهة KittenDataSource في الوحدة: أحدهما لنظام Android والآخر لنظام iOS. لقد ذكرت بالفعل أنهم مسؤولون عن الوصول إلى الشبكة. لكن لديهم في الواقع وظيفة أخرى: يقومون بإنشاء عنوان URL للطلب بناءً على وسيطات الإدخال "limit" و "الصفحة". في الوقت نفسه ، لدينا فئة KittenStoreNetwork لا تفعل شيئًا سوى تفويض المكالمة إلى KittenDataSource.
التعيين: انقل منطق إنشاء طلب URL من KittenDataSourceImpl (على Android و iOS) إلى KittenStoreNetwork. تحتاج إلى تغيير واجهة KittenDataSource على النحو التالي:
بمجرد الانتهاء من ذلك ، ستحتاج إلى تحديث اختباراتك. الفصل الوحيد الذي تحتاج إلى لمسه هو TestKittenDataSource.
مضيفا تحميل الصفحة
يدعم TheCatAPI ترقيم الصفحات ، لذا يمكننا إضافة هذه الوظيفة لتجربة مستخدم أفضل. يمكنك البدء بإضافة حدث Event.EndReached جديد لـ KittenView ، وبعد ذلك سيتوقف الرمز عن التجميع. بعد ذلك ستحتاج إلى إضافة Intent.LoadMore المناسب ، وتحويل الحدث الجديد إلى Intent ، ومعالجة الأخير في KittenStoreImpl. ستحتاج أيضًا إلى تعديل واجهة KittenStoreImpl.Network على النحو التالي:
أخيرًا ، ستحتاج إلى تحديث بعض تطبيقات الاختبار ، وإصلاح اختبار واحد أو اختبارين حاليين ، ثم كتابة بعض الاختبارات الجديدة لتغطية ترقيم الصفحات.
