الأدلة التقنية

اختبار تطبيقات الجوال مع Appium وMaestro

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

اختبار تطبيقات الجوال مع Appium وMaestro

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

وقد شرحنا في مقال أنواع الاختبارات للمبتدئين أنواع الاختبارات وهرم الاختبارات Test Pyramid والاختبارات المتقلبة Flaky Tests، وكل ذلك ينطبق على الجوال كما هو، فاختبارات الوحدة Unit Tests لمنطق التطبيق تبقى الأكثر عدداً والأسرع، وهذا المقال يركز على الطبقة العليا التي تختلف في الجوال: اختبار واجهة التطبيق UI Test على جهاز حقيقي أو محاكي Emulator، وأدواته، وأين تشغله.

وسوف نناقش في هذا المقال ما يلي:

  • كيف تتحكم أدوات اختبار الجوال في التطبيق، ولماذا يحدد ذلك سرعتها وما تستطيع فعله.
  • أداة Appium وأداة Maestro وأداتا المنصة Espresso وXCUITest، ورخصة كل منها، ومتى تناسب كل واحدة.
  • المحاكي Emulator مقابل الجهاز الحقيقي، ومزارع الأجهزة Device Farms وأسعارها.
  • تجربة عملية كاملة: محاكي Android رسمي داخل Docker، ثم نفس السيناريو بأداة Appium 3 وأداة Maestro، مع إدخال نص عربي، ثم نكسر اختباراً مبنياً على الإحداثيات بتحويل الواجهة إلى اتجاه اليمين إلى اليسار RTL.

ما الذي يجعل اختبار الجوال مختلفاً؟

في اختبار الويب أنت تتحكم في المتصفح، وهو برنامج واحد تعرف إصداره وتشغله في CI بأمر واحد، أما في الجوال فهناك ثلاثة أشياء إضافية يجب أن تفكر فيها قبل أن تكتب أول اختبار:

  • الجهاز نفسه: التطبيق يحتاج نظام Android أو iOS يعمل، إما محاكي Emulator على جهازك أو في CI، أو جهاز حقيقي موصول بكابل USB أو عبر الشبكة، وتشغيل المحاكي يحتاج تسريع العتاد Hardware Acceleration، وهذا ما سوف تراه في التجربة.
  • تثبيت التطبيق: الاختبار يعمل على ملف APK أو AAB في Android، وملف IPA أو حزمة App في iOS، أي على نسخة مبنية من التطبيق، وليس على رابط كما في الويب.
  • حالة النظام: الأذونات Permissions، ونوافذ النظام، ولغة الجهاز واتجاه الكتابة، والوضع الليلي، ودوران الشاشة، والتطبيق الذي ينتقل إلى الخلفية ثم يعود، وكلها أشياء يمكن أن تفشل فيها الواجهة حتى لو كان منطق التطبيق سليماً.

ولذلك فالسؤال الأول ليس «أي أداة؟» وإنما «أين يعمل الاختبار بالنسبة إلى التطبيق؟»، لأن هذا يحدد كل شيء آخر.

كيف تتحكم الأدوات في التطبيق؟

الصورة التالية تبين الطرق الثلاث التي تعمل بها أدوات اختبار واجهات الجوال:

مخطط من ثلاثة صفوف: في Appium يرسل الاختبار أوامر WebDriver إلى سيرفر Appium الذي يمررها إلى مشغل UiAutomator2 أو XCUITest على الجهاز ثم إلى التطبيق، وفي Maestro يقرأ سطر الأوامر ملف flow.yaml ويتعامل مع شجرة الوصول عبر تطبيق مساعد صغير على الجهاز، وفي Espresso وXCUITest يبنى كود الاختبار مع التطبيق نفسه ويعمل بجانبه على الجهاز
كلما اقتربت الأداة من التطبيق عرفت حالته أكثر، وكلما ابتعدت عنه صارت تعمل مع أي تطبيق دون الحاجة إلى الكود

في الإعداد أعلاه لاحظ التالي:

  • Appium: الاختبار يرسل أوامر بروتوكول WebDriver، وهو نفس بروتوكول Selenium، إلى سيرفر Appium، والسيرفر يمررها إلى مشغل Driver خاص بالمنصة، ففي Android المشغل الشائع هو UiAutomator2 الذي يثبت تطبيقاً صغيراً على الجهاز يستخدم إطار UiAutomator من Google، وفي iOS المشغل هو XCUITest الذي يبني على إطار Apple نفسه. والنتيجة أن الاختبار يكتب بأي لغة لها مكتبة WebDriver، مثل JavaScript وJava وPython وC#، ولا يحتاج كود التطبيق.
  • Maestro: الاختبار ملف YAML فيه قائمة أوامر مثل tapOn وinputText وassertVisible، وأداة سطر الأوامر تتعامل مع التطبيق عبر طبقة الوصول Accessibility Layer للنظام، أي نفس المعلومات التي يقرؤها قارئ الشاشة، وتنتظر تلقائياً حتى تستقر الواجهة. ولا تحتاج كود التطبيق أيضاً.
  • Espresso وXCUITest: هذه أدوات المنصة نفسها، فالاختبار يكتب بلغة التطبيق، Kotlin أو Java في Android وSwift في iOS، ويبنى مع التطبيق بأداة Gradle أو Xcode، ويعمل بجانبه على الجهاز، وبالتالي يعرف متى انتهى التطبيق من عمله ويكون أسرع وأكثر ثباتاً، لكنه يحتاج كود التطبيق ويكتبه عادة المطور نفسه.

الأدوات واحدة واحدة

أداة Appium

Appium مشروع مفتوح المصدر برخصة Apache-2.0، وهو الأقدم والأوسع في هذا المجال، وفكرته أن تختبر تطبيقات الجوال بنفس الطريقة التي تختبر بها المتصفح عبر Selenium. وقد صدر Appium 2 في يوليو 2023 وفصل المشغلات Drivers عن السيرفر، فأصبحت تثبت السيرفر ثم تثبت المشغل الذي تحتاجه بأمر appium driver install، ثم صدر Appium 3 في أغسطس 2025، وأهم تغييراته بحسب دليل الانتقال من 2 إلى 3 أنه يحتاج Node.js 20.19 أو أحدث، وحذف نقاط نهاية Endpoints قديمة كانت مهجورة، وأصبحت الميزات غير الآمنة Insecure Features تحتاج اسم المشغل قبلها. وفي التجربة استخدمنا Appium 3.8.0 مع مشغل UiAutomator2 بالإصدار 8.7.0.

  • نقاط القوة: يعمل مع Android وiOS وأنظمة أخرى عبر مشغلات مختلفة، وتكتب الاختبار بلغتك المفضلة، ويختبر أي تطبيق مبني دون الحاجة إلى كوده، ويدعمه أغلب مزودي مزارع الأجهزة، ومعه أداة Appium Inspector لاستكشاف عناصر الشاشة.
  • نقاط الضعف: طبقات كثيرة بين الاختبار والتطبيق، فالجلسة تبدأ ببطء نسبياً والإعداد أطول، والانتظار مسؤوليتك أنت، فإذا لم تكتب انتظاراً صحيحاً حصلت على اختبارات متقلبة. واختبار iOS يحتاج جهاز macOS عليه Xcode بحسب صفحة مشغل XCUITest.

أداة Maestro

Maestro أداة أحدث برخصة Apache-2.0، وتقول صفحة المشروع إنها بنيت على ما تعلمه أصحابها من Appium وEspresso وXCTest وغيرها، وإنها اتخذت ثلاثة قرارات من البداية: التحكم في التطبيق عبر طبقة الوصول في النظام، والتثبيت كملف واحد دون مشغلات، وأن يكون الاختبار قائمة أوامر YAML مسطحة. وتعمل الاختبارات بحسب الصفحة نفسها على المحاكيات وعلى أجهزة Android الحقيقية، أما أجهزة iOS الحقيقية فليست مدعومة بعد، ويحتاج سطر الأوامر Java 17 أو أحدث.

  • نقاط القوة: اختبار مقروء يكتبه شخص من فريق الجودة QA دون خبرة برمجية كبيرة، وانتظار تلقائي حتى تستقر الواجهة، وتثبيت بسيط، ويعمل مع التطبيقات الأصلية Native وReact Native وFlutter بنفس الأوامر.
  • نقاط الضعف: لغة YAML محدودة عندما يحتاج الاختبار منطقاً معقداً، فتلجأ إلى ملفات JavaScript مساعدة، ولا يدعم أجهزة iOS الحقيقية حتى الآن، وأداة Maestro Studio وخدمة Maestro Cloud ليستا مفتوحتي المصدر. ويرسل سطر الأوامر إحصاءات استخدام مجهولة إلا إذا عرفت المتغير MAESTRO_CLI_NO_ANALYTICS، وقد عرفناه في التجربة.

أداتا المنصة Espresso وXCUITest

Espresso إطار اختبار الواجهة من Google لنظام Android، وهو جزء من مكتبات AndroidX Test المفتوحة برخصة Apache-2.0، وأهم ما فيه التزامن التلقائي Synchronization، فبحسب وثائقه ينتظر Espresso قبل كل فعل حتى تفرغ قائمة رسائل الواجهة ولا يوجد عمل خلفي جار، وبالتالي لا تحتاج إلى sleep في الاختبار. وتقول الوثائق نفسها إن قوته الكاملة تظهر مع من يعرف كود التطبيق، أي أنه أداة المطور. وفي iOS المقابل هو XCUITest، أي اختبارات الواجهة في إطار XCTest من Apple، وهو جزء من Xcode ومجاني لكنه ليس مفتوح المصدر، ويحتاج جهاز Mac.

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

الأدوات في جدول واحد

المعيارAppiumMaestroEspresso / XCUITest
الرخصةApache-2.0Apache-2.0 (سطر الأوامر)Espresso برخصة Apache-2.0، وXCUITest جزء من Xcode
لغة الاختبارأي لغة لها مكتبة WebDriverملفات YAMLKotlin أو Java، وSwift
يحتاج كود التطبيق؟لالانعم
الانتظارتكتبه أنتتلقائيتلقائي عبر التزامن مع الواجهة
Android وiOS باختبار واحدنعم، مع اختلاف المحدداتنعملا، اختبار لكل منصة
أجهزة iOS الحقيقيةنعم، من جهاز Macغير مدعومة بعدنعم، من جهاز Mac
من يكتبه عادةمهندس أتمتة الاختبارفريق الجودة QA أو المطورالمطور

المحاكي أم الجهاز الحقيقي؟

المحاكي Emulator في Android برنامج يشغل نظام Android كاملاً على جهازك كآلة افتراضية، والمقابل في iOS هو Simulator الذي يأتي مع Xcode ويعمل على جهاز Mac فقط. وكلاهما مجاني وسريع البدء ويمكن تشغيله في CI بإعداد ثابت كل مرة، لكنه لا يشبه هاتف المستخدم في كل شيء، فالمعالج والذاكرة مختلفان، ولا توجد كاميرا حقيقية ولا مستشعرات حقيقية، ولا توجد التعديلات التي تضيفها الشركات المصنعة على Android، ولا ضعف الشبكة الحقيقي.

وقد يتساءل البعض: إذا كان الجهاز الحقيقي أدق، فلماذا لا نختبر عليه دائماً؟ والإجابة أن الجهاز الحقيقي بطيء في التجهيز ومكلف في الشراء والصيانة، ولا يمكن تشغيل عشرة منه بالتوازي في CI بسهولة، لذلك فالتقسيم المعقول هو أن تعمل الاختبارات الآلية على المحاكي في كل طلب دمج Pull Request، ثم على عدد قليل من الأجهزة الحقيقية قبل كل إصدار، كما تبين الصورة التالية:

ثلاثة أعمدة: الأول محاكي في CI يعمل مع كل طلب دمج وهو سريع ومجاني لكنه لا يكشف مشاكل المعالج الرسومي والكاميرا والبطارية وتعديلات الشركات المصنعة، والثاني بضعة أجهزة حقيقية قبل كل إصدار لاختبار الأجهزة التي يملكها المستخدمون فعلاً وللفحص اليدوي، والثالث مزرعة أجهزة في السحابة تعمل ليلاً أو قبل الإصدار بمئات الأجهزة المستأجرة مقابل سعر للدقيقة أو للجهاز ويخرج فيها التطبيق وبياناته من شبكتك
نفس الاختبارات تعمل في الأماكن الثلاثة، والفرق في عدد المرات والتكلفة وما يكشفه كل مكان

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

مزارع الأجهزة Device Farms

مزرعة الأجهزة خدمة تستأجر منها أجهزة حقيقية في مركز بيانات، فترفع ملف التطبيق والاختبارات، وتختار الطرازات، وتحصل على النتائج والفيديو والسجلات، وهي مفيدة عندما تحتاج طرازات كثيرة لا يمكنك شراؤها. والأسعار التالية من الصفحات الرسمية كما رأيناها في 12 أكتوبر 2026، وهي تتغير، فراجعها قبل أن تقرر:

الخدمةطريقة التسعيرالسعر المعلن
AWS Device Farmبالدقيقة لكل جهاز، أو جهاز مخصص بالشهر0.17 دولار للدقيقة، وأول 1000 دقيقة مجانية، وخانة جهاز Device Slot بلا حد للاستخدام تبدأ من 250 دولاراً شهرياً
Firebase Test Labبالساعة بعد حصة يومية مجانيةفي خطة Blaze أول 60 دقيقة يومياً على المحاكيات و30 دقيقة على الأجهزة الحقيقية مجانية، ثم دولار واحد للساعة على المحاكي و5 دولارات للساعة على الجهاز الحقيقي، وتحسب بالدقيقة
BrowserStack وخدمات مشابهةاشتراك شهري لكل اختبار متواز Parallelالأسعار حسب الخطة وعدد الاختبارات المتوازية، فراجع صفحة التسعير

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

كيف تختار؟

  • فريق تطوير صغير يملك كود تطبيق Android أو iOS واحد: ابدأ باختبارات الوحدة ثم Espresso أو XCUITest لأهم ثلاثة أو أربعة مسارات، والسبب أنها الأسرع والأكثر ثباتاً ويكتبها المطور بجانب الكود.
  • تطبيق React Native أو Flutter، أو فريق جودة QA يكتب الاختبارات: Maestro غالباً أسرع طريق إلى أول اختبار ثابت، لأن الانتظار تلقائي والملف مقروء، وتستطيع تشغيل نفس الملف على Android وiOS.
  • فريق أتمتة لديه اختبارات Selenium، أو تحتاج أجهزة iOS حقيقية في مزرعة، أو منطقاً معقداً في الاختبار: Appium هو الخيار، لأنه معيار تدعمه كل المزارع، وتكتب فيه بلغة برمجة كاملة.
  • موقع ويب يفتح على الجوال وليس تطبيقاً: لا تحتاج أياً من هذه الأدوات في البداية، فمحاكاة الأجهزة في Playwright التي ذكرناها في أنواع الاختبارات تحاكي حجم الشاشة ووكيل المستخدم User Agent واللمس بحسب وثائق المحاكاة Emulation، لكنها تبقى متصفحاً يعمل على جهازك، وليست متصفح الهاتف الحقيقي.

تجربة عملية: محاكي Android في Docker مع Appium وMaestro

سوف نشغل محاكي Android الرسمي من Google داخل Container، ثم نكتب نفس السيناريو مرتين، مرة بأداة Appium 3 ومرة بأداة Maestro، على تطبيق ApiDemos، وهو نسخة من تطبيق أمثلة Android يستخدمها مشروع Appium في اختباراته وترخيصه Apache-2.0. والسيناريو: افتح Views ثم Controls ثم 1. Light Theme، واكتب اسماً بالإنجليزية ثم نصاً عربياً في حقل النص، ثم علم على المربع Checkbox 1 وتأكد من النتيجة.

📌
التجربة تعمل على Windows 11 مع Docker Desktop وWSL2، ولم نغير أي إعداد في Windows أو WSL، والسبب أن الآلة الافتراضية التي يشغلها Docker Desktop كانت تعرض الجهاز /dev/kvm أصلاً، فتأكد من ذلك على جهازك بأمر docker run --rm --privileged alpine ls -l /dev/kvm قبل أن تبدأ، فإذا لم يظهر الجهاز فالمحاكي سوف يعمل دون تسريع، أي ببطء شديد لا يصلح للاختبار، كما تشرح صفحة تسريع المحاكي.

Docker Image واحدة فيها المحاكي والأداتان

هناك مشاريع جاهزة تشغل المحاكي في Docker، وأشهرها docker-android، لكن رخصته Apache-2.0 مع تعديلات خاصة، والنسخة المجانية ترسل بحسب ملف الرخصة إحصاءات استخدام إلى Google Forms، منها المدينة والدولة المستنتجة من عنوان IP، إلا إذا عرفت المتغير USER_BEHAVIOR_ANALYTICS=false، وإصدارات Android الأحدث فيه للرعاة فقط. لذلك بنينا Docker Image بأنفسنا من أدوات Google الرسمية، فكل شيء فيها مثبت الإصدار ومعروف المصدر:

# One image for the whole lab: the official Android emulator (headless, needs /dev/kvm),
# Appium 3 with the UiAutomator2 driver, and the Maestro CLI. Everything pinned.
FROM eclipse-temurin:21.0.12.1_1-jdk-noble

ARG CMDLINE_TOOLS=15859902
ARG NODE_VERSION=22.23.3
ARG APPIUM_VERSION=3.8.0
ARG UIA2_VERSION=8.7.0
ARG MAESTRO_VERSION=2.11.0
ARG SYSTEM_IMAGE="system-images;android-35;google_apis;x86_64"

ENV ANDROID_HOME=/opt/android \
    ANDROID_SDK_ROOT=/opt/android \
    APPIUM_HOME=/opt/appium \
    MAESTRO_CLI_NO_ANALYTICS=1 \
    MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED=true
ENV PATH=/opt/node/bin:/opt/maestro/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH

RUN apt-get update && apt-get install -y --no-install-recommends \
      unzip curl xz-utils libpulse0 libgl1 libnss3 libxcomposite1 libxcursor1 libxdamage1 \
      libxi6 libxtst6 libasound2t64 libxkbfile1 libdbus-1-3 \
    && rm -rf /var/lib/apt/lists/*

# Android SDK command-line tools, then platform-tools, build-tools (Appium needs apksigner), the emulator and one system image
RUN mkdir -p $ANDROID_HOME/cmdline-tools && cd /tmp \
    && curl -fsSLo tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.zip \
    && unzip -q tools.zip && mv cmdline-tools $ANDROID_HOME/cmdline-tools/latest && rm tools.zip \
    && yes | sdkmanager --licenses > /dev/null \
    && sdkmanager --install "platform-tools" "emulator" "platforms;android-35" "build-tools;35.0.0" "$SYSTEM_IMAGE" > /dev/null \
    && echo no | avdmanager create avd -n lab -k "$SYSTEM_IMAGE" -d pixel_6

# Node.js, Appium 3 and the UiAutomator2 driver
RUN curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | tar -xJ -C /opt \
    && mv /opt/node-v${NODE_VERSION}-linux-x64 /opt/node \
    && npm install -g appium@${APPIUM_VERSION} \
    && appium driver install uiautomator2@${UIA2_VERSION}

# Maestro CLI
RUN curl -fsSLo /tmp/maestro.zip https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip \
    && unzip -q /tmp/maestro.zip -d /opt && rm /tmp/maestro.zip

WORKDIR /lab

في الإعداد أعلاه لاحظ التالي:

  • صورة النظام google_apis;x86_64 للإصدار Android 15 (API 35)، وهي تسمح بصلاحيات root في المحاكي، على عكس صور Google Play.
  • حزمة build-tools ضرورية لأداة Appium، ففي أول تشغيل بدونها رفض Appium بدء الجلسة بالخطأ التالي، ثم ثبتناها وأضفناها إلى الملف.
  • المتغير MAESTRO_CLI_NO_ANALYTICS يوقف إحصاءات Maestro، وقد تحققنا من اسمه في كود الأداة نفسها.
  • عند تثبيت الحزم طبع sdkmanager تحذيراً بأنه أصبح مهجوراً Deprecated وأن البديل أداة سطر أوامر جديدة اسمها android، والأمر ما زال يعمل.
WebDriverError: Cannot verify the signature of '/apk/ApiDemos-debug.apk'. Original error: Could not find 'apksigner.jar' in [...]. Do you have Android Build Tools installed at '/opt/android'?

والآن نبني ونشغل الـ Container، مع تمرير جهاز /dev/kvm وحده بدلاً من وضع --privileged، ومجلد فيه ملف التطبيق:

docker build -t lab-qa3-mob-android:1 .
docker run -d --name lab-qa3-mob-android --device /dev/kvm \
  -v "$PWD/apk:/apk:ro" -v "$PWD:/lab" lab-qa3-mob-android:1 sleep infinity
docker exec lab-qa3-mob-android sh -c 'appium -v; maestro --version; emulator -version | head -1'

والمخرج سوف يكون كما يلي:

3.8.0
2.11.0
Android emulator version 37.2.12.0 (build_id 16428233) (CL:N/A)

واستغرق البناء عندنا قرابة 30 دقيقة، أغلبها تنزيل صورة النظام وفك طبقات Docker Image التي بلغ حجمها على القرص 9.7 GB، لذلك في CI احفظ هذه Docker Image في سجل خاص Registry ولا تقم ببنائها في كل تشغيل.

تشغيل المحاكي دون شاشة

السكربت التالي يشغل المحاكي دون نافذة، وينتظر حتى ينتهي Android من الإقلاع، ثم يطفئ الحركات Animations ويثبت التطبيق:

#!/usr/bin/env bash
# Starts the emulator headless inside the container and waits until Android has finished booting.
set -e
start=$(date +%s)
emulator -avd lab -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect \
  -memory 3072 -cores 4 > /tmp/emulator.log 2>&1 &
adb wait-for-device
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do sleep 2; done
echo "boot completed in $(( $(date +%s) - start ))s"
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
adb install -r /apk/ApiDemos-debug.apk
adb shell getprop ro.build.version.release

والمخرج سوف يكون كما يلي:

boot completed in 117s
Performing Streamed Install
Success
15

أي أن الإقلاع البارد استغرق قرابة دقيقتين، وأثناء الاختبارات استهلك الـ Container قرابة 6.4 GB من الذاكرة، وهذا رقم يجب أن تحسبه عند اختيار سيرفر CI، فلا تقم بتشغيل أكثر من محاكي أو اثنين على سيرفر بذاكرة 16 GB.

السيناريو بأداة Appium

نشغل سيرفر Appium داخل الـ Container على 127.0.0.1:4723، ونكتب الاختبار بمكتبة WebdriverIO 10.0.2:

// Appium 3 + UiAutomator2: Views > Controls > 1. Light Theme, type English then Arabic, tick a checkbox.
//   node appium-controls.mjs        (Appium server on 127.0.0.1:4723 in the same container)
import { remote } from 'webdriverio';

const t0 = Date.now();
const driver = await remote({
  hostname: '127.0.0.1', port: 4723, logLevel: 'error',
  capabilities: {
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:app': '/apk/ApiDemos-debug.apk',
    'appium:appPackage': 'io.appium.android.apis',
    'appium:appActivity': '.ApiDemos',
    'appium:newCommandTimeout': 120,
  },
});
console.log(`session ready in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
try {
  const t1 = Date.now();
  await driver.$('~Views').click();                       // accessibility id = content-desc
  await driver.$('~Controls').click();
  await driver.$('~1. Light Theme').click();
  const edit = driver.$('id=io.appium.android.apis:id/edit');
  await edit.setValue('Wajdy');
  console.log('english :', await edit.getText());
  await edit.setValue('مرحبا بك');
  console.log('arabic  :', await edit.getText());
  const box = driver.$('id=io.appium.android.apis:id/check1');
  await box.click();
  console.log('checked :', await box.getAttribute('checked'));
  console.log(`steps took ${((Date.now() - t1) / 1000).toFixed(1)}s`);
} finally {
  await driver.deleteSession();
}

والمخرج سوف يكون كما يلي:

session ready in 9.7s
english : Wajdy
arabic  : مرحبا بك
checked : true
steps took 18.9s

في الإعداد أعلاه لاحظ التالي:

  • المحدد ~Views يبحث بمعرف الوصول Accessibility ID، وهو في Android الخاصية content-desc، والمحدد id=… يبحث بمعرف المورد Resource ID، وكلاهما لا يتغير مع اللغة أو حجم الشاشة.
  • النص العربي وصل إلى الحقل كما هو.
  • الوقت يتغير بين تشغيل وآخر، ففي تشغيل ثان بلغ بدء الجلسة 34.3 ثانية والخطوات 16.9 ثانية، والجهاز كان يشغل تجارب أخرى في الوقت نفسه، فاعتبر الأرقام توضيحية.

السيناريو نفسه بأداة Maestro

الملف التالي يكتب نفس الخطوات بلغة Maestro:

# Same scenario as appium-controls.mjs: open Views > Controls > 1. Light Theme,
# type a name in English and in Arabic, tick the first checkbox and check the result.
appId: io.appium.android.apis
---
- launchApp:
    clearState: true
- tapOn: "Views"
- tapOn: "Controls"
- tapOn: "1. Light Theme"
- tapOn:
    id: "io.appium.android.apis:id/edit"
- inputText: "Wajdy"
- assertVisible: "Wajdy"
- eraseText
- inputText: "مرحبا بك"
- assertVisible: "مرحبا بك"
- tapOn:
    id: "io.appium.android.apis:id/check1"
- assertVisible:
    id: "io.appium.android.apis:id/check1"
    checked: true

والمخرج سوف يكون كما يلي:

Assert that "Wajdy" is visible... COMPLETED
Erase text... COMPLETED
Input text مرحبا بك... COMPLETED
Assert that "مرحبا بك" is visible... FAILED

Assertion is false: "مرحبا بك" is visible

لاحظ أن أمر الكتابة نفسه انتهى بحالة COMPLETED، لكن الحقل بقي فارغاً، فقد قرأنا شجرة الواجهة بعد الفشل فوجدنا الحقل يعرض النص الإرشادي hint text فقط. والسبب مكتوب في وثائق الأمر inputText، فالأحرف خارج ASCII، أي Unicode، غير مدعومة في Android، مع إحالة إلى التذكرة رقم 146. وهذه نقطة مهمة لأي تطبيق عربي، فلو لم يكن في الملف أمر assertVisible بعد الكتابة لنجح الاختبار وهو لم يكتب شيئاً. والحل الذي نجح معنا هو تمرير النص عبر الحافظة Clipboard بأمري setClipboard وpasteText:

# Arabic text through the clipboard instead of inputText (inputText does not type Unicode on Android).
appId: io.appium.android.apis
---
- launchApp:
    clearState: true
- tapOn: "Views"
- tapOn: "Controls"
- tapOn: "1. Light Theme"
- setClipboard: "مرحبا بك"
- tapOn:
    id: "io.appium.android.apis:id/edit"
- pasteText
- assertVisible: "مرحبا بك"
- tapOn:
    id: "io.appium.android.apis:id/check1"
- assertVisible:
    id: "io.appium.android.apis:id/check1"
    checked: true

والمخرج سوف يكون كما يلي:

Set Maestro clipboard to مرحبا بك... COMPLETED
Tap on id: io.appium.android.apis:id/edit... COMPLETED
Paste text... COMPLETED
Assert that "مرحبا بك" is visible... COMPLETED
Tap on id: io.appium.android.apis:id/check1... COMPLETED
Assert that id: io.appium.android.apis:id/check1 is visible... COMPLETED

real	1m57.042s

وقد يتساءل البعض: لماذا استغرق Maestro قرابة دقيقتين بينما استغرق Appium نصف دقيقة؟ والإجابة في سجل Maestro، فقرابة دقيقة ذهبت بين تشغيل الأداة وأول أمر، أي في تشغيل Java وتجهيز الاتصال بالجهاز، ثم أخذ كل نقر بين 5 و15 ثانية، لأن Maestro ينتظر بعد كل فعل حتى تستقر الشاشة، والمحاكي هنا يرسم بالمعالج الرئيسي عبر swiftshader_indirect دون معالج رسومي GPU، والجهاز مشغول بتجارب أخرى. وهذا الانتظار التلقائي هو نفسه ما يجعل Maestro ثابتاً دون sleep، فأنت تدفع ثباته من وقته، وعلى جهاز أسرع سوف يقل الفرق.

نكسر الاختبار: النقر بالإحداثيات مع واجهة عربية

بعض الفرق تسجل الاختبار بأداة تحفظ مكان النقر على الشاشة بدلاً من العنصر، والاختبار التالي يفعل الشيء نفسه عمداً، فينقر على النقطة 159,537 وهي منتصف المربع Checkbox 1 في الواجهة الإنجليزية، ثم يكرر النقر عبر معرف العنصر، ونشغله مرة بلغة الجهاز الإنجليزية ومرة بالعربية، حيث يغير Appium لغة الجهاز بالخاصيتين appium:language وappium:locale:

// Break-it: the same check done two ways, by screen coordinates and by resource id.
//   node appium-coords.mjs en      → device in English (LTR)
//   node appium-coords.mjs ar      → device in Arabic (RTL), changed by Appium through the Appium Settings app
import { remote } from 'webdriverio';

const lang = process.argv[2] || 'en';
const driver = await remote({
  hostname: '127.0.0.1', port: 4723, logLevel: 'error',
  capabilities: {
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:app': '/apk/ApiDemos-debug.apk',
    'appium:appPackage': 'io.appium.android.apis',
    'appium:appActivity': '.view.Controls1',
    'appium:language': lang,
    'appium:locale': lang === 'ar' ? 'SA' : 'US',
  },
});
const ID = 'id=io.appium.android.apis:id/check1';
try {
  await driver.$(ID).waitForDisplayed({ timeout: 20000 });
  const r = { ...(await driver.$(ID).getLocation()), ...(await driver.$(ID).getSize()) };
  console.log(`[${lang}] Checkbox 1 is at x=${r.x}..${r.x + r.width}, y=${r.y}..${r.y + r.height}`);

  // 1) the recorded tap: x=159, y=537 is the centre of Checkbox 1 on the English screen
  await driver.action('pointer', { parameters: { pointerType: 'touch' } })
    .move({ x: 159, y: 537 }).down().up().perform();
  console.log(`[${lang}] tap by coordinates  -> checked=${await driver.$(ID).getAttribute('checked')}`);

  // 2) reset the screen, then the same tap through the element itself
  await driver.execute('mobile: startActivity', { intent: 'io.appium.android.apis/.view.Controls1', stop: true });
  await driver.$(ID).waitForDisplayed({ timeout: 20000 });
  await driver.$(ID).click();
  console.log(`[${lang}] tap by resource id  -> checked=${await driver.$(ID).getAttribute('checked')}`);
} finally {
  await driver.deleteSession();
}

والمخرج سوف يكون كما يلي:

[en] Checkbox 1 is at x=0..318, y=446..629
[en] tap by coordinates  -> checked=true
[en] tap by resource id  -> checked=true

[ar] Checkbox 1 is at x=762..1080, y=446..629
[ar] tap by coordinates  -> checked=false
[ar] tap by resource id  -> checked=true

التطبيق يعلن في ملف Manifest أنه يدعم الاتجاه من اليمين إلى اليسار supportsRtl=true، فعندما أصبحت لغة الجهاز عربية انعكست الواجهة كاملة، وانتقل المربع من يسار الشاشة إلى يمينها، كما تبين الصورة التالية من المحاكي نفسه:

لقطة شاشة من المحاكي بلغة عربية: شاشة Light Theme في تطبيق ApiDemos وقد انعكست، فالمربعات وأزرار الاختيار على يمين الشاشة، وزرا التبديل يعرضان كلمة إيقاف بالعربية، والساعة بالأرقام العربية
نفس الشاشة بعد تحويل لغة الجهاز إلى العربية: العناصر انتقلت إلى اليمين، فالنقرة المسجلة على اليسار تقع على مساحة فارغة

فالنقرة بالإحداثيات وقعت على مساحة فارغة دون أي رسالة خطأ، ولم يعرف الاختبار أنه نقر على لا شيء إلا عند التحقق. وقد كررنا الشيء نفسه في Maestro بالأمر tapOn مع point، فكانت النتيجة نفسها:

# Break-it in Maestro: the recorded tap on Checkbox 1 by its pixel position on the English screen.
appId: io.appium.android.apis
---
- launchApp
- tapOn: "Views"
- tapOn: "Controls"
- tapOn: "1. Light Theme"
- tapOn:
    point: "159,537"
- assertVisible:
    id: "io.appium.android.apis:id/check1"
    checked: true
Tap on point (159,537)... COMPLETED
Assert that id: io.appium.android.apis:id/check1 is visible... FAILED

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

ما الذي لم نشغله

لم نشغل Espresso لأنه يحتاج كود التطبيق ومشروع Gradle، ولم نشغل XCUITest ولا محاكي iOS لأنهما يحتاجان جهاز Mac، ولم نستخدم جهازاً حقيقياً ولا مزرعة أجهزة لأننا لم ننشئ أي حساب خارجي، فأرقام المزارع في هذا المقال من صفحات الأسعار فقط.

إزالة التجربة

docker rm -f lab-qa3-mob-android
docker rmi lab-qa3-mob-android:1

أخطاء شائعة

  • الضغط على إحداثيات الشاشة: اختبار يضغط على نقطة ثابتة يعمل على شاشة واحدة واتجاه واحد فقط، وينكسر مع جهاز آخر أو مع العربية، كما رأيت في التجربة، فاستخدم المعرف Resource ID أو معرف الوصول Accessibility ID.
  • البحث عن العناصر بالنص المعروض: النص يتغير مع الترجمة ومع تعديل صياغة الواجهة، فإذا كان تطبيقك بالعربية والإنجليزية فالنص محدد هش، والمعرف أثبت منه.
  • الانتظار بـ sleep ثابت: أبطأ من اللازم على الجهاز السريع وأقل من اللازم على المحاكي البطيء في CI، فاستخدم الانتظار حتى يظهر العنصر، أو أداة تنتظر تلقائياً.
  • ترك الحركات Animations مفعلة في CI: الحركات تبطئ الاختبار وتسبب نقرات قبل أن يستقر العنصر، لذلك أطفأناها في التجربة بثلاثة أوامر adb.
  • اختبار التطبيق بلغة واحدة فقط: إذا كان جمهورك عربياً فشغل مسار الاختبار الأساسي مرة باللغة العربية واتجاه RTL، فهناك تظهر النصوص المقطوعة والأيقونات غير المعكوسة.
  • كتابة كل شيء كاختبار واجهة: اختبار الواجهة على الجوال بطيء، فالمنطق مكانه اختبارات الوحدة، واختبار الواجهة لأهم المسارات فقط، مثل التسجيل والدفع.
  • رفع نسخة فيها مفاتيح الإنتاج إلى مزرعة أجهزة: استخدم نسخة اختبار Build Variant تتصل ببيئة اختبار وبيانات وهمية.

قائمة تحقق

  • اختبارات الوحدة تغطي منطق التطبيق، واختبارات الواجهة لأهم المسارات فقط.
  • كل عنصر يحتاجه الاختبار له معرف ثابت، Resource ID في Android أو Accessibility Identifier في iOS، وهذا يساعد قارئ الشاشة أيضاً.
  • الاختبارات تعمل على محاكي في كل طلب دمج، بإصدار نظام ثابت ومعروف، والحركات مطفأة.
  • مسار أساسي واحد على الأقل يعمل باللغة العربية واتجاه RTL.
  • قائمة الأجهزة الحقيقية مبنية على بيانات المستخدمين، وفيها جهاز ضعيف واحد على الأقل.
  • إصدارات الأدوات مثبتة في CI، Appium والمشغل وMaestro وصورة النظام، حتى لا يتغير شيء دون أن تعرف.
  • نسخة الاختبار لا تحتوي مفاتيح الإنتاج ولا بيانات عملاء حقيقية.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • اختبار واجهة الجوال طبقة فوق اختبارات الوحدة وليس بديلاً عنها، فاكتبه لأهم المسارات فقط.
  • Espresso وXCUITest أسرع وأثبت لكنهما يحتاجان كود التطبيق ويكتبان مرة لكل منصة، وAppium يعمل مع أي تطبيق مبني وبأي لغة وتدعمه المزارع، وMaestro أسهل كتابة وينتظر تلقائياً، لكنه لا يكتب Unicode في Android بأمر inputText، فاستخدم الحافظة أو تحقق بعد كل كتابة.
  • محاكي Android الرسمي يعمل في Docker إذا كان /dev/kvm متاحاً، وأقلع عندنا في قرابة دقيقتين واستهلك قرابة 6.4 GB من الذاكرة، فاحسب ذلك في سيرفر CI.
  • المحاكي لكل طلب دمج، وبضعة أجهزة حقيقية مختارة من بيانات المستخدمين قبل كل إصدار، ومزرعة أجهزة عندما تحتاج طرازات كثيرة، مع الانتباه إلى أن التطبيق وبياناته يخرجان من شبكتك.
  • الاختبار المبني على الإحداثيات ينكسر بصمت عند تحويل الواجهة إلى العربية، والمعرف الثابت لكل عنصر يحل ذلك ويساعد قارئ الشاشة أيضاً.
دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.دليلاختبار سهولة الوصول Accessibility لموقع عربيما الذي تجده أدوات فحص سهولة الوصول وما الذي يفوتها، وما الذي يخص المواقع العربية، وكيف تمنع عودة المشاكل في خط CI.دليلاختبار العقود Contract Testing بين الخدمات مع Pactكيف تمنع خدمة من كسر خدمة أخرى تعتمد عليها قبل النشر، بعقد يكتبه المستهلك ويتحقق منه المزود ويحفظه Pact Broker.دليلاختبار الطفرات Mutation Testing: هل تكشف اختباراتك الأخطاء؟لماذا تمر الأخطاء من اختبارات تغطيتها 100%، وكيف يقيس اختبار الطفرات قوتها فعلاً ويعمل في خط CI دون أن يبطئه.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Docker Engine 29.8.0 مع Docker Desktop وWSL2، بمحاكي Android 37.2.12 وصورة النظام Android 15 (API 35، google_apis x86_64 الإصدار 9) وplatform-tools 37.0.1 وbuild-tools 35.0.0، وAppium 3.8.0 مع مشغل UiAutomator2 8.7.0 وWebdriverIO 10.0.2 على Node.js 22.23.3، وMaestro CLI 2.11.0 على Java 21.0.12.1 (eclipse-temurin:21.0.12.1_1-jdk-noble)، وتطبيق ApiDemos 6.0.18.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.