إنّ الطبيعة غير المتزامنة للتطبيقات وإطارات العمل المتوافقة مع الأجهزة الجوّالة تجعل من الصعب في كثير من الأحيان كتابة اختبارات موثوقة وقابلة للتكرار. عند إدخال حدث مستخدم، يجب أن ينتظر إطار عمل الاختبار انتهاء استجابة التطبيق له، والذي يمكن أن يتراوح بين تغيير بعض النصوص على الشاشة وإعادة إنشاء نشاط بالكامل. عندما لا يكون للاختبار سلوك حتمي، يكون غير متّسق.
تم تصميم الأطر الحديثة، مثل Compose أو Espresso، مع الأخذ في الاعتبار الاختبار، وبالتالي تتوفر ضمانة معيّنة بأنّ واجهة المستخدم ستكون في وضع السكون قبل إجراء الاختبار التالي أو العبارة التالية. وهذا ما يُعرف باسم المزامنة.
مزامنة الاختبار
قد تستمر المشاكل في الظهور عند تنفيذ عمليات غير متزامنة أو عمليات في الخلفية غير معروفة للاختبار، مثل تحميل البيانات من قاعدة بيانات أو عرض متحركين أبديين.
لزيادة موثوقية مجموعة الاختبار، يمكنك تثبيت طريقة لتتبُّع العمليات التي تعمل في الخلفية، مثل Espresso Idling Resources. يمكنك أيضًا استبدال وحدات الإصدارات الاختبارية التي يمكنك الاستعلام عنها بحثًا عن الخمول أو التي تُحسِّن المزامنة، مثل TestDispatcher لعمليات التشغيل المتعدّدة في وقت واحد أو RxIdler لـ RxJava.
طرق تحسين الثبات
يمكن أن ترصد الاختبارات الكبيرة الكثير من حالات التراجع في الأداء في الوقت نفسه لأنّها تختبر مكونات متعددة للتطبيق. ويتم تشغيلها عادةً على المحاكيات أو الأجهزة، ما يشير إلى أنّها تتسم بدقة عالية. على الرغم من أنّ الاختبارات الكبيرة الشاملة من البداية إلى النهاية تقدّم تغطية كاملة، إلا أنّها أكثر عرضة للأعطال العرضية.
تشمل التدابير الأولية التي يمكنك اتخاذها لتقليل التقشرات ما يلي:
- ضبط الأجهزة بشكل صحيح
- منع مشاكل المزامنة
- تنفيذ عمليات إعادة المحاولة
لإنشاء اختبارات كبيرة باستخدام Compose أو Espresso، تبدأ عادةً أحد نشاطاتك وتتنقل كما يفعل المستخدم، للتأكّد من أنّ واجهة المستخدم تعمل بشكلٍ صحيح باستخدام التأكيدات أو اختبارات لقطات الشاشة.
تتيح لك أُطر العمل الأخرى، مثل UI Automator، نطاقًا أوسع، إذ يمكنك التفاعل مع واجهة مستخدم النظام والتطبيقات الأخرى. ومع ذلك، قد تتطلّب اختبارات UI Automator المزيد من المزامنة اليدوية، لذا تكون عادةً أقل موثوقية.
ضبط الأجهزة
أولاً، لتحسين موثوقية اختباراتك، عليك التأكّد من أنّه لن يتم بشكل غير متوقّع إيقاف تنفيذ اختباراتك من قِبل نظام التشغيل على الجهاز. على سبيل المثال، عندما يظهر مربّع حوار تحديث النظام فوق التطبيقات الأخرى أو عندما تكون المساحة على القرص غير كافية.
يضبط مزوّدو مجموعات الأجهزة الأجهزة والمحاكيات، لذا لا يلزمك عادةً اتخاذ أي إجراء. ومع ذلك، قد يكون لها توجيهات خاصة بها لإعدادات الحالات الخاصة.
الأجهزة المُدارة من خلال Gradle
إذا كنت تدير المحاكيات بنفسك، يمكنك استخدام الأجهزة التي تديرها Gradle لتحديد الأجهزة التي تريد استخدامها للقيام بإجراء اختباراتك:
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
// Use device profiles you typically see in Android Studio.
device = "Pixel 2"
// Use only API levels 27 and higher.
apiLevel = 30
// To include Google services, use "google".
systemImageSource = "aosp"
}
}
}
}
}
باستخدام هذه الإعدادات، سيؤدي الأمر التالي إلى إنشاء صورة محاكي، وبدء تشغيل مثيل، وإجراء الاختبارات وإغلاقه.
./gradlew pixel2api30DebugAndroidTest
وتحتوي الأجهزة التي تديرها Gradle على آليات لإعادة المحاولة في حال إلغاء ربط الجهاز وإجراء تحسينات أخرى.
منع مشاكل المزامنة
يمكن أن تؤدي المكوّنات التي تُجري عمليات في الخلفية أو عمليات غير متزامنة إلى تعذُّر اختبار العبارة لأنّه تم تنفيذ عبارة الاختبار قبل أن يصبح واجهة المستخدم جاهزة لها. ومع توسع نطاق الاختبار، تزيد من فرص أن يصبح غير مستقر. إنّ مشاكل التنسيق هذه هي مصدر رئيسي للمشاكل غير المُتّسقة لأنّ منصات اختبار الأداء تحتاج إلى استنتاج ما إذا كان قد انتهى تحميل نشاط معيّن أو ما إذا كان يجب الانتظار لفترة أطول.
الحلول
يمكنك استخدام موارد وضع السكون في Espresso للإشارة إلى الحالات التي يكون فيها التطبيق مشغولاً، ولكن من الصعب تتبُّع كل عملية غير متزامنة، خاصةً في الاختبارات الشاملة المكثفة للغاية. بالإضافة إلى ذلك، قد يكون من الصعب تثبيت الموارد غير النشطة بدون تلويث الرمز البرمجي الذي يتم اختباره.
بدلاً من تقدير ما إذا كان النشاط مشغولاً أم لا، يمكنك جعل الاختبارات تنتظر إلى أن تستوفي شروطًا معيّنة. على سبيل المثال، يمكنك الانتظار حتى يتم عرض نص أو مكون محدد في واجهة المستخدم.
يشمل Compose مجموعة من واجهات برمجة التطبيقات الاختبارية كجزء من
ComposeTestRule
لانتظار نتائج مطابقة مختلفة:
fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)
وواجهة برمجة تطبيقات عامة تأخذ أي دالة تعرض قيمة منطقية:
fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit
مثال على الاستخدام:
composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>
آليات إعادة المحاولة
يجب إصلاح الاختبارات غير الموثوق بها، ولكن في بعض الأحيان تكون الظروف التي تؤدي إلى تعذّر إجرائها غير محتمَلة جدًا. على الرغم من أنّه يجب دائمًا متابعة الاختبارات غير المستقرة وإصلاحها، يمكن أن تساعد آلية إعادة المحاولة في الحفاظ على انتاجية المطوّر من خلال إجراء الاختبار عدة مرات إلى أن يجتازه.
يجب إجراء عمليات إعادة المحاولة على مستويات متعدّدة لتجنُّب حدوث مشاكل، مثل:
- انتهت مهلة الاتصال بالجهاز أو انقطع الاتصال
- تعذّر إجراء اختبار واحد
يعتمد تثبيت عمليات إعادة المحاولة أو ضبطها على إطارات عمل الاختبار و البِنية الأساسية، ولكن تشمل الآليات المعتادة ما يلي:
- قاعدة JUnit التي تعيد محاولة إجراء أي اختبار عدة مرات
- إجراء أو خطوة لإعادة المحاولة في سير عمل التطوير المتكامل
- نظام لإعادة تشغيل المحاكي عندما لا يستجيب، مثل الأجهزة المُدارة من Gradle