أتمتة اختبار تطبيقات الجوال
أتمتة اختبار الجوال (Mobile test automation) تقود التطبيق على محاكٍ أو جهاز حقيقي وتتحقق من سلوكه دون أن ينقر شخص على الشاشة. وتغطي ثلاث أدوات معظم المجال: Espresso لـ Android وXCUITest لـ iOS، وهما إطارا اختبار الواجهة الخاصان بالمنصتين، وAppium، وهو خادم مفتوح المصدر يقود المنصتين عبر بروتوكول WebDriver. وتختلف الثلاث في مكان تشغيل الاختبار، وكيفية انتظاره للتطبيق، ومن يكتبه عادةً، وإلى أي مدى يستطيع الوصول إلى الجهاز.
يقارن هذا الشرح بين الثلاث، ويعرض اختباراً مختصراً بكل منها، ويغطي الأجزاء التي تحدد هل تحظى حزمة اختبارات الجوال بالثقة أم تُهمَل: المحددات، وعدم الاستقرار، ومزارع الأجهزة، وCI. أما السياق الأوسع — استراتيجية الأجهزة ومناطق المخاطر والأمان — ففي الدليل المحوري لاختبار الجوال.
ماذا تؤتمت على الجوال
ينطبق هرم الاختبار (Test pyramid) على الجوال كما على أي منتج (يشرح دليل الاختبار اليدوي مقابل الآلي السبب): معظم الفحوص مكانها تحت الواجهة، حيث تكون سريعة ومستقرة. في Android تعمل اختبارات الوحدة المحلية على JVM في جهاز التطوير خلال ثوانٍ، بينما تعمل الاختبارات المُجهَّزة (Instrumented) على جهاز أو محاكٍ؛ وفي iOS يشغّل XCTest اختبارات الوحدة واختبارات الواجهة من مشروع Xcode نفسه. قواعد العمل والتحقق والحسابات مكانها اختبارات الوحدة واختبارات API على الواجهة الخلفية. أما اختبارات الواجهة فللرحلات التي يجب ألا تنكسر أبداً — تسجيل الدخول، والتهيئة الأولى، والبحث، وإتمام الشراء، والميزة الأساسية للتطبيق — وللربط بين الشاشات.
- اختبارات الوحدة — نماذج العرض، والمنسّقات، والتحقق، واستعلامات قاعدة البيانات المحلية؛ تعمل مع كل إيداع.
- الاختبارات المُجهَّزة أو التكاملية — الشيفرة التي تحتاج المنصة: التخزين، وقاعدة البيانات المحلية، والعمل في الخلفية؛ تعمل على المحاكيات.
- اختبارات API — الواجهة الخلفية التي يستدعيها التطبيق، بما في ذلك استجابات الأخطاء والتوافق مع العملاء القدامى.
- اختبارات الواجهة — مجموعة صغيرة من الرحلات الحرجة لكل منصة، تعمل في CI على المحاكيات وعلى أجهزة حقيقية قبل الإصدار.
- الاختبار اليدوي والاستكشافي — الإيماءات، والمقاطعات الحقيقية، والمظهر والإحساس، وسلوك الأجهزة الحقيقية (انظر قسم ما يبقى يدوياً).
مقارنة Espresso وXCUITest وAppium
لا يوجد فائز مطلق. إذا كان المطورون يملكون الاختبارات وتبني فرق منفصلة كل منصة، فالأطر الأصلية هي الأنسب: أسرع تغذية راجعة، وأقل عدم استقرار، واللغة نفسها لغة التطبيق. وإذا كان فريق أتمتة مستقل يغطي المنصتين بمسارات مشتركة، أو كان لا بد من تشغيل الاختبارات على بناءات شبيهة بالإصدار دون وصول إلى الشيفرة، فإن قاعدة Appium الواحدة وحرية اختيار اللغة تصبحان أهم. وكثير من الفرق تجمع بينها: أطر أصلية لاختبارات مستوى الشاشة في خط المطورين، ومجموعة Appium صغيرة متعددة المنصات للرحلات من البداية إلى النهاية.
- Espresso — لـ Android فقط؛ الاختبارات بلغة Kotlin أو Java؛ يعمل داخل عملية التطبيق كاختبار مُجهَّز. يتزامن تلقائياً مع خيط واجهة التطبيق، فلا يتصرف إلا حين تكون الواجهة خاملة؛ والعمل في الخلفية الذي لا يراه يُسجَّل عبر موارد الخمول (Idling resources). سريع ومستقر؛ ويكتبه عادةً مطورو Android بجوار الشيفرة. ولا يستطيع الوصول إلى واجهة النظام خارج التطبيق — وهذا ما يغطيه UI Automator.
- XCUITest — لـ iOS فقط؛ الاختبارات بلغة Swift أو Objective-C، تُكتب في Xcode مع XCTest (وتوثّق Apple الآن واجهة اختبار الواجهة باسم XCUIAutomation). مشغّل الاختبار عملية منفصلة تقود التطبيق عبر طبقة إمكانية الوصول، كما يفعل المستخدم. ينتظر استقرار التطبيق قبل التفاعل ويستطيع التعامل مع تنبيهات النظام. ويكتبه عادةً مطورو iOS أو مهندسو أتمتة يعملون في Xcode.
- Appium — لـ Android وiOS ومنصات أخرى؛ الاختبارات بأي لغة لها عميل WebDriver (Java وPython وJavaScript وC# وRuby…). يرسل العميل أوامر W3C WebDriver إلى خادم Appium، الذي يستخدم مشغّل منصة (Driver) — مثل مشغّل UiAutomator2 على Android ومشغّل XCUITest على iOS — للتصرف على الجهاز. يعامل التطبيق كصندوق أسود، ويستطيع الانتقال إلى سياقات WebView في التطبيقات الهجينة، ويستطيع أتمتة متصفحات الجوال. كل خطوة تعبر طبقات أكثر، فهو أبطأ من الأطر الأصلية.
EspressoAndroid فقط
داخل عملية التطبيق
- 1الاختبار: إيجاد عنصر، إجراء، تأكيداختبار مُجهَّزينتظر خمول خيط الواجهة
- 2عناصر واجهة التطبيق
- لغة الاختبار:
- Kotlin أو Java
- كيف ينتظر:
- تلقائياً حتى يخمل خيط الواجهة؛ والعمل في الخلفية الذي لا يراه يُسجَّل عبر موارد الخمول
- يكتبه عادةً:
- مطورو Android بجوار الشيفرة
- مدى الوصول:
- داخل التطبيق فقط؛ وواجهة النظام تحتاج UI Automator
- السرعة:
- سريع ومستقر
XCUITestiOS فقط
- 1مشغّل الاختبارعملية منفصلةعبر طبقة إمكانية الوصول
- 2التطبيقشجرة عناصره، يُستعلم عنها حسب النوع والمعرّف
- لغة الاختبار:
- Swift أو Objective-C
- كيف ينتظر:
- ينتظر استقرار التطبيق؛ وللعناصر التي تظهر لاحقاً:
waitForExistence(timeout:) - يكتبه عادةً:
- مطورو iOS أو مهندسو أتمتة يعملون في Xcode
- مدى الوصول:
- التطبيق، وتنبيهات النظام أيضاً
- السرعة:
- أصلي: تغذية راجعة أسرع من Appium
AppiumAndroid وiOS وغيرهما
- 1شيفرة الاختبار + عميل WebDriverأوامر W3C WebDriver
- 2خادم Appium
- 3مشغّل المنصةمثل مشغّل UiAutomator2 على Android، ومشغّل XCUITest على iOS
- 4التطبيق على الجهازيُعامَل كصندوق أسود
- لغة الاختبار:
- أي لغة لها عميل WebDriver
- كيف ينتظر:
- انتظار صريح لشرط، مثل وجود العنصر
- يكتبه عادةً:
- يناسب فريق أتمتة مستقلاً يغطي المنصتين
- مدى الوصول:
- التطبيق كصندوق أسود، وسياقات WebView في التطبيقات الهجينة، ومتصفحات الجوال
- السرعة:
- أبطأ: كل خطوة تعبر طبقات أكثر
Espresso: داخل العملية ومتزامن
يجد اختبار Espresso عنصراً بواسطة مُطابِق (Matcher)، وينفّذ عليه إجراءً، ثم يتحقق من تأكيد. وقبل كل إجراء وكل تأكيد ينتظر Espresso حتى يخلو طابور رسائل خيط الواجهة، وهذا يزيل معظم شيفرة الانتظار التي تحتاجها الأدوات الأخرى. أما العمل الذي يجري في مكان آخر — مجمّع خيوط مخصص، أو مكتبة شبكة لا يعرفها Espresso — فيجب تسجيله كمورد خمول، وإلا تسابق معه الاختبار. والشاشات المبنية بـ Jetpack Compose تُختبر بواجهات الاختبار الخاصة بـ Compose، التي تتبع المبدأ نفسه.
@RunWith(AndroidJUnit4::class)
class LoginTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun wrongPasswordShowsError() {
onView(withId(R.id.email)).perform(typeText("layla.h@example.com"), closeSoftKeyboard())
onView(withId(R.id.password)).perform(typeText("wrong-pass-1"), closeSoftKeyboard())
onView(withId(R.id.sign_in)).perform(click())
onView(withText(R.string.error_invalid_credentials)).check(matches(isDisplayed()))
}
}XCUITest: صندوق أسود عبر إمكانية الوصول
يشغّل اختبار XCUITest التطبيق كعملية منفصلة ويستعلم عن شجرة عناصره حسب النوع والمعرّف: الأزرار وحقول النص والنصوص الثابتة. يضع المطورون accessibilityIdentifier على كل عنصر تحتاجه الاختبارات؛ وعلى خلاف تسمية إمكانية الوصول (Accessibility label)، لا ينطق VoiceOver المعرّف، فيمكن أن يكون اسماً تقنياً ثابتاً. وتضع وسائط التشغيل ومتغيرات البيئة التطبيقَ في حالة اختبار — عنوان خادم وهمي، أو مخزن بيانات مُعاد ضبطه، أو حركات معطّلة — دون بناءات خاصة. استخدم waitForExistence(timeout:) للعناصر التي تظهر بعد عمل غير متزامن بدلاً من الانتظار الثابت.
final class LoginUITests: XCTestCase {
func testWrongPasswordShowsError() {
let app = XCUIApplication()
app.launchArguments += ["-uiTesting", "-resetState"]
app.launch()
let email = app.textFields["login.email"]
email.tap()
email.typeText("layla.h@example.com")
let password = app.secureTextFields["login.password"]
password.tap()
password.typeText("wrong-pass-1")
app.buttons["login.submit"].tap()
XCTAssertTrue(app.staticTexts["login.error"].waitForExistence(timeout: 5))
}
}Appium: بروتوكول WebDriver للجوال
Appium خادم. يفتح اختبارك، المكتوب بمكتبة عميل WebDriver، جلسةً بإرسال القدرات (Capabilities) — أي منصة، وأي مشغّل أتمتة، وأي تطبيق، وأي جهاز — ثم يرسل أوامر مثل إيجاد عنصر والنقر وإرسال المفاتيح. ومنذ Appium 2 تُثبَّت المشغّلات منفصلة عن الخادم، فيحدد المشروع بدقة إصدارات المشغّلات التي يستخدمها. والقدرات غير الواردة في معيار W3C تحمل البادئة appium:. ولأن Appium يقود التطبيق من الخارج، يمكن تشغيل الاختبار نفسه على بناء تصحيح في محاكٍ وعلى مرشّح إصدار في هاتف من مزرعة أجهزة.
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
caps = {
"platformName": "Android",
"appium:automationName": "UiAutomator2",
"appium:deviceName": "Pixel_Test",
"appium:app": "/builds/shop-4.0.0-debug.apk",
"appium:autoGrantPermissions": True,
}
driver = webdriver.Remote("http://127.0.0.1:4723", options=UiAutomator2Options().load_capabilities(caps))
try:
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.email").send_keys("layla.h@example.com")
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.password").send_keys("wrong-pass-1")
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.submit").click()
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "login.error")))
finally:
driver.quit()المحددات على الجوال
المحدد (Locator) هو الطريقة التي يجد بها الاختبار عنصراً، وعلى الجوال يحدد اختياره الاستقرار والسرعة معاً. اتفق مع المطورين على اصطلاح للمعرّفات (مثلاً screen.element: login.submit)، وضع المعرّفات على كل عنصر تلمسه الاختبارات، واجعل «وجود معرّفات الاختبار» جزءاً من تعريف الإنجاز (Definition of done). المبادئ العامة في دليل استراتيجيات المحددات؛ وفيما يلي خصوصيات الجوال.
- معرّف إمكانية الوصول (Accessibility id) — الاستراتيجية المفضلة في Appium. على iOS يطابق accessibilityIdentifier، الذي لا يقرؤه VoiceOver، فيمكن أن يكون اسماً تقنياً. وعلى Android يطابق وصف المحتوى (Content description)، الذي يقرؤه TalkBack بصوت عالٍ — لذا يجب أن يكون على Android تسمية بشرية حقيقية، أو أن تستخدم معرّف المورد بدلاً منه.
- معرّف المورد (Resource id) في Android — مثل com.example.shop:id/sign_in؛ ثابت ما دام المطورون لا يغيّرون المعرّفات.
- الاستعلامات الأصلية للمنصة — UiSelector في UiAutomator2، وسلاسل Predicate وClass chain في iOS — حين لا يناسب ما سبق؛ وهي أسرع من XPath.
- النص الظاهر — ينكسر حين يُوطَّن التطبيق (بناءات العربية والإنجليزية تعرض نصوصاً مختلفة)؛ لا تستخدمه إلا مع موارد لكل لغة.
- XPath — الملاذ الأخير: بطيء على الجوال لأن شجرة العرض كلها يجب أن تُسلسَل، وهشّ حين يتغير التخطيط.
- الأطر متعددة المنصات (React Native وFlutter) توفر خاصية معرّف اختبار خاصة بها؛ تحقق مما تُطابقه على كل منصة قبل بناء المحددات عليها.
لماذا تتذبذب اختبارات واجهة الجوال — وماذا تفعل
الاختبار المتذبذب (Flaky test) ينجح ويفشل على الشيفرة نفسها. ويضيف الجوال أسبابه الخاصة إلى الأسباب المعتادة (عدم التزامن، والحالة المشتركة، والخدمات البعيدة، والوقت)، والحزمة التي تفشل عشوائياً سرعان ما تُهمَل. يغطي دليل الاختبارات المتذبذبة الاكتشاف والحجر؛ وهذه هي المصادر الخاصة بالجوال.
- الحركات والانتقالات — عطّلها على أجهزة الاختبار والمحاكيات (إعدادات مقياس الحركة الثلاثة أدناه)، أو عبر وسائط التشغيل في iOS.
- عدم تزامن لا يراه الإطار — سجّل موارد الخمول في Espresso، وانتظر الشروط (العنصر موجود، ومفعّل) في كل مكان آخر؛ ولا تستخدم الانتظار الثابت أبداً.
- حوارات النظام — طلبات الأذونات، و«التطبيق لا يستجيب»، وتلميحات لوحة المفاتيح، وطلبات تحديث النظام. امنح الأذونات مسبقاً حيث لا يكون الاختبار عنها، وتعامل صراحةً مع تنبيهات النظام المتوقعة.
- تسرّب الحالة بين الاختبارات — جلسة مسجّلة، وبيانات محلية، وأعلام ميزات. أعد الضبط لكل اختبار: امسح بيانات التطبيق (يستطيع Android Test Orchestrator مسح بيانات الحزمة بين الاختبارات) أو شغّل بوسيط إعادة ضبط في iOS.
- الشبكة الحقيقية وواجهة خلفية مشتركة — استخدم خادماً وهمياً أو بيئة اختبار مُهيّأة ببيانات، وولّد بيانات فريدة لكل تشغيل.
- لوحة المفاتيح التي تغطي العنصر التالي، والعناصر التي تحتاج تمريراً لتظهر.
- صحة الأجهزة والمحاكيات — إقلاع بطيء، وتخزين ممتلئ، وبطارية منخفضة، أو أجهزة مزرعة ساخنة. أضف فحوص صحة؛ وأعد المحاولة على مستوى البنية التحتية فقط، لا لإخفاء إخفاق حقيقي.
- فروق اللغة والمنطقة الزمنية والساعة بين أجهزة المصفوفة.
# Android — turn off animations on a test device or emulator
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جرّبها: مولّد استجابات API وهمية جرّبها: محاكي رموز حالة API جرّبها: مولّد استجابات خطأ وهمية جرّبها: مولّد مستخدمين وهميين
مزارع الأجهزة وخطوط CI
تعمل محاكيات Android على أجهزة CI بنظام Linux أو Windows أو macOS، وتحتاج تسريعاً عتادياً لتكون سريعة (KVM على Linux)؛ أما محاكيات iOS فتتطلب macOS. قسّم الحزم الطويلة إلى أجزاء (Shards) تعمل بالتوازي على عدة محاكيات أو أجهزة. وتأخذ مزرعة الأجهزة السحابية تطبيقك وحزمة اختباراته، وتشغّلها على الأجهزة الحقيقية التي تختارها، وتعيد النتائج مع مقاطع فيديو ولقطات شاشة وسجلات؛ والأجهزة نفسها متاحة عادةً لجلسات يدوية. اختر أجهزة المزرعة من مصفوفة أجهزتك، واستخدم الاختيار الثنائي (Pairwise) للمعاملات الثانوية (اللغة والاتجاه وحجم الخط) بدلاً من مضاعفة التشغيلات.
انشر النتائج بصيغة يفهمها خادم CI (وJUnit XML هي الشائعة) واحتفظ بمخرجات كل اختبار فاشل. البناء الأحمر الذي لا يرافقه فيديو وسجل جهاز هو بناء سيعيد أحدهم تشغيله بدلاً من إصلاحه.
ويعرض المثال التوضيحي أدناه إيقاعاً ممكناً للخط: مع كل طلب دمج (Pull request) تعمل اختبارات الوحدة، ويُبنى التطبيق وحزمة الاختبار، وتعمل حزمة دخان من 10 إلى 20 اختباراً حرجاً على محاكٍ واحد لكل منصة، واختبارات عقود API مقابل الواجهة الخلفية في بيئة Staging. وكل ليلة تعمل حزمة الواجهة كاملة مقسّمة على المحاكيات، مع أجهزة الطبقتين 1 و2 (Tier 1 وTier 2) في مزرعة أجهزة. وقبل الإصدار يُختبر مرشّح الإصدار على أجهزة الطبقتين 1 و2 (والطبقة 3 أيضاً قبل الإصدارات الكبرى) مع جلسة استكشافية على أجهزة حقيقية، ويُشغَّل أقدم إصدار مدعوم من التطبيق مقابل الواجهة الخلفية الجديدة. والطبقات هي طبقات مصفوفة الأجهزة النموذجية في الدليل المحوري لاختبار الجوال. ولكل اختبار فاشل: فيديو ولقطات شاشة وسجل الجهاز وJUnit XML.
Example mobile pipeline
on every pull request
1. unit tests (JVM / XCTest unit)
2. build the app and the test package (test APK / UI test bundle)
3. UI smoke: 10–20 critical tests on one Android emulator and one iOS simulator
4. API contract tests against the staging back end
nightly
5. full UI suite, sharded across emulators and simulators
6. Tier 1 and Tier 2 real devices in a device farm
before release
7. release candidate on the Tier 1 and Tier 2 devices (Tier 3 too before a major release)
+ exploratory session on real devices
8. oldest supported app version against the new back end (API smoke)
for every failed test: video, screenshots, device log, JUnit XMLجرّبها: مولّد الاختبار الزوجي (Pairwise) جرّبها: مدقّق مخططات API
ما يبقى يدوياً
تتحقق الأتمتة مما تعرف مسبقاً أن تسأل عنه. وعلى الجوال ما زالت عدة أسئلة مهمة يجيب عنها شخص بجهاز حقيقي أفضل من غيره: كيف تبدو الإيماءات عند الاستخدام، وهل يبدو التخطيط صحيحاً على جهاز لم يكتب له أحد نصاً آلياً، وماذا يحدث أثناء مكالمة حقيقية على شبكة حقيقية، وهل التطبيق قابل للاستخدام بقارئ الشاشة. توجد فحوص آلية لإمكانية الوصول في المنصتين — يستطيع Espresso تشغيل فحوص إمكانية الوصول أثناء الاختبارات، ويستطيع XCUITest تشغيل تدقيق لإمكانية الوصول — وهي تكشف التسميات الناقصة وأهداف اللمس الصغيرة والتباين المنخفض، لكنها لا تحل محل جلسة مع TalkBack أو VoiceOver.
- جلسات استكشافية على أجهزة حقيقية لكل مرشّح إصدار.
- مقاطعات حقيقية: مكالمات، وBluetooth، وإشعارات من تطبيقات أخرى.
- مراجعة بصرية عبر الأجهزة، ما لم تكن تصون اختبارات لقطات الشاشة.
- جلسات قارئ الشاشة والنص الكبير.
- البطارية والحرارة والأداء على أضعف جهاز.
- بناءات المتجر: التثبيت من المتجر أو القناة التجريبية تماماً كما يفعل المستخدمون.
الأسئلة الشائعة
Appium أم Espresso — أيهما أستخدم؟
Espresso إذا كان مطورو Android يكتبون الاختبارات وتريد أسرع تغذية راجعة وأكثرها استقراراً. وAppium إذا كان فريق أتمتة واحد يغطي Android وiOS بمسارات مشتركة، أو يفضّل لغة واحدة، أو يجب أن يختبر بناءات شبيهة بالإصدار كصندوق أسود. وكثير من الفرق تستخدم الاثنين.
هل XCUITest هو نفسه XCTest؟
XCTest هو إطار Apple لاختبارات الوحدة والأداء والواجهة. وXCUITest هو الاسم الشائع لجزء اختبار الواجهة فيه، الذي توثّقه Apple الآن باسم XCUIAutomation: اختبارات تشغّل التطبيق وتتفاعل معه عبر إمكانية الوصول.
هل يستطيع Appium اختبار التطبيقات الهجينة وويب الجوال؟
نعم. يستطيع Appium الانتقال من السياق الأصلي إلى سياق WebView في التطبيق الهجين واستخدام محددات الويب هناك، ويستطيع أتمتة متصفحات الجوال مثل Chrome على Android وSafari على iOS.
لماذا نستخدم معرّفات إمكانية الوصول كمحددات؟
لأنها مستقلة عن التخطيط والنص الظاهر، فتصمد أمام إعادة التصميم والتوطين، والبحث عنها سريع. وتذكّر أن معرّف إمكانية الوصول على Android هو وصف المحتوى الذي يقرؤه TalkBack، فيجب أن يكون تسمية حقيقية أو أن تستخدم معرّفات الموارد.
كيف أشغّل اختبارات واجهة الجوال في CI؟
شغّل حزمة دخان صغيرة على محاكي Android (مع تسريع عتادي) ومحاكي iOS (على مشغّلات macOS) مع كل طلب دمج، والحزمة الكاملة ليلاً مقسّمة إلى أجزاء، وأجهزة حقيقية في مزرعة أجهزة قبل الإصدار، مع الاحتفاظ بفيديو وسجلات الإخفاقات.
كيف أقلل تذبذب اختبارات واجهة الجوال؟
عطّل الحركات، واعتمد على مزامنة الإطار والانتظار المشروط بدلاً من الانتظار الثابت، وأعد ضبط حالة التطبيق لكل اختبار، وامنح الأذونات مسبقاً، واستخدم واجهة خلفية وهمية أو مُهيّأة ببيانات، وحافظ على صحة الأجهزة. واحجر الاختبارات المتذبذبة وأصلح السبب.
المراجع
- Appium Documentationالتوثيق الرسمي لأتمتة التطبيقات الأصلية والهجينة وتطبيقات الويب على iOS وAndroid.
- Espressoإطار اختبار واجهات Android الذي يزامن إجراءات الاختبار مع خيط واجهة التطبيق تلقائياً.
- XCUIAutomationواجهة Apple البرمجية لاختبارات الواجهة التي تشغّل التطبيق وتتفاعل مع عناصره كما يفعل المستخدم (أساس XCUITest).
- XCTestإطار Apple لاختبارات الوحدة والأداء والواجهة لمشاريع Xcode على iOS وبقية منصات Apple.
- Test apps on Androidإرشادات Android الرسمية للاختبار: أنواع الاختبارات ونطاقاتها، والاختبارات المحلية مقابل المُجهَّزة على الجهاز، وأفضل الممارسات.
- Eradicating Non-Determinism in Tests (Martin Fowler)لماذا تُفقد الاختبارات المتذبذبة الحزمةَ قيمتها، وأسبابها الشائعة (العزل وعدم التزامن والخدمات البعيدة والوقت وتسرّب الموارد)، والحجر كاستجابة.
- Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0منهج ISTQB المتقدم لتصميم حلول أتمتة الاختبار وتنفيذها وصيانتها وبنيتها.
- Certified Tester Mobile Application Testing (CT-MAT)منهج ISTQB التخصصي لاختبار تطبيقات الجوال: أنواع التطبيقات وتحديات الأجهزة والمنصات وأنواع الاختبار والأدوات.
تمرين عملي
اجعل اختبار Appium المتذبذب لرمز الخصم مستقراً
تختبر تطبيق توصيل بقالة (مثال خيالي) على Android وiOS بواجهة عربية وإنجليزية. يضيف الإصدار 7.2 رموز الخصم إلى السلة، وأضاف مهندس أتمتة اختبار Appium المعروض أدناه إلى حزمة اختبارات الواجهة الليلية، التي تعمل في أربعة أجزاء (Shards) متوازية على هواتف في مزرعة أجهزة. وعلى مدى آخر 40 ليلة عمل الاختبار مرة كل ليلة، ففشل في 14 تشغيلاً من أصل 40، وبدأ الفريق يتجاهل النتائج الليلية، والإصدار قريب. تعرض المادة التهيئة، والاختبار بأرقام أسطره، وكل تشغيل فاشل مع ما يُظهره فيديو المزرعة، وملاحظتين من الفريق.
اعمل على الورق مستعيناً بأقسام الدليل عن الطبقات والمحددات وعدم الاستقرار ومزارع الأجهزة، وأشِر إلى أرقام الأسطر. ينتظر جوابَك شخصان: ريم، مالكة المنتج، تريد أن تعرف هل يعمل الرمز SAVE20، وعمر، أحد المطورين، يقترح إعادة تشغيل الاختبارات الفاشلة تلقائياً حتى ثلاث مرات. خصّص لذلك 45–60 دقيقة.
تطبيق توصيل بقالة (مثال خيالي) - Android وiOS، بواجهة عربية وإنجليزية
الإصدار 7.2، وبناء Android هو 7.2.0-rc1
قاعدة الخصم PR-4: الرمز SAVE20 يخصم 20% من المجموع الفرعي للسلة.
حزمة اختبارات الواجهة الليلية: pytest مع Appium 2 ومشغّل UiAutomator2. تنقسم الحزمة إلى
4 أجزاء (Shards) تعمل بالتوازي، جزء لكل هاتف في مزرعة الأجهزة: الهواتف A وB وC وD.
كل ليلة تضع خطة توزيع الأجزاء كل اختبار في جزء واحد، فيعمل الاختبار مرة واحدة كل ليلة.
يشغّل كل جزء نصيبه من tests/ui بالأمر pytest دون أي خيارات، ولا يُحفظ من التشغيل
إلا فيديو المزرعة.
الهاتف C منخفض المواصفات، وشاشته أصغر شاشة في المصفوفة.
تعطي مصفوفة الأجهزة بالاختيار الثنائي (Pairwise) كل تشغيل لغةً: en أو ar.
تسجّل الأجزاء كلها الدخول بحساب الاختبار نفسه: qa.shopper@example.com
test_checkout.py
1 import time
2 from appium import webdriver
3 from appium.options.android import UiAutomator2Options
4 from appium.webdriver.common.appiumby import AppiumBy
5
6 CAPS = {
7 "platformName": "Android",
8 "appium:automationName": "UiAutomator2",
9 "appium:app": "/builds/grocer-7.2.0-rc1.apk",
10 "appium:noReset": True,
11 }
12
13 def test_apply_promo_code(device_name):
14 # precondition: test_login has already signed in as qa.shopper@example.com
15 # and the cart holds one item: rice 5 kg, SAR 100.00
16 caps = {**CAPS, "appium:deviceName": device_name}
17 driver = webdriver.Remote(
18 "http://127.0.0.1:4723",
19 options=UiAutomator2Options().load_capabilities(caps))
20 driver.find_element(AppiumBy.XPATH,
21 "//android.widget.FrameLayout[2]/android.widget.LinearLayout"
22 "/android.widget.TextView[3]").click() # Cart tab
23 time.sleep(3)
24 promo = driver.find_element(AppiumBy.ID, "com.example.grocer:id/promo_input")
25 promo.send_keys("SAVE20")
26 driver.find_element(AppiumBy.XPATH,
27 '//android.widget.Button[@text="Apply"]').click()
28 time.sleep(2)
29 total = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "cart.total").text
30 assert total == "SAR 80.00"
31 driver.quit()
آخر 40 ليلة: شُغّل test_apply_promo_code أربعين مرة، فنجح 26 مرة وفشل 14 مرة.
F1-F5 التشغيلات الخمسة بلغة ar (على الهواتف الأربعة)
NoSuchElementException: //android.widget.Button[@text="Apply"]
الفيديو: شاشة السلة بالعربية؛ الزر «تطبيق»، والمجموع «100.00 ر.س»
F6-F8 الهاتف C، لغة en (نجحت تشغيلات en الأخرى على الهاتف C)
AssertionError: assert 'SAR 100.00' == 'SAR 80.00'
الفيديو: SAVE20 في الحقل؛ لوحة المفاتيح تبقى مفتوحة وتغطي زر «Apply»
F9-F10 الهاتف B، لغة en
AssertionError: assert 'SAR 100.00' == 'SAR 80.00'
الفيديو: مؤشر تحميل على المجموع، ثم يصبح SAR 80.00 بعد انتهاء الاختبار
سجل Staging: استغرق طلب الخصم في هاتين الليلتين 2.6 ثانية و3.1 ثانية
F11-F12 الهاتف A، لغة en
AssertionError: assert 'SAR 140.00' == 'SAR 80.00'
الفيديو: في السلة 3 أصناف، والمجموع الفرعي SAR 175.00؛
أضاف اختبار في جزء آخر صنفين منها أثناء تشغيل هذا الاختبار
F13 الهاتف D، لغة en
NoSuchElementException: (محدد XPath لتبويب السلة، الأسطر 20-22)
الفيديو: حوار النظام «Allow Grocer to send you notifications?»
F14 الهاتف B، لغة en
NoSuchElementException: (محدد XPath لتبويب السلة، الأسطر 20-22)
الفيديو: شاشة تسجيل الدخول؛ في تلك الليلة وضعت خطة توزيع الأجزاء
test_login على الهاتف D
ملاحظات الفريق
ريم (مالكة المنتج): «قبل أن نُطلق 7.2: هل يعمل SAVE20 أم لا؟»
عمر (مطوّر): «إنه متذبذب فحسب. لنُعِد تشغيل الاختبارات الفاشلة تلقائياً حتى 3 مرات
لتصبح النتيجة الليلية خضراء».- جمّع التشغيلات الفاشلة الأربعة عشر في مجموعات. ولكل مجموعة اذكر مصدرها من الدليل (المحدد، أو عدم التزامن، أو لوحة المفاتيح، أو حوار النظام، أو تسرّب الحالة بين الاختبارات، أو الواجهة الخلفية المشتركة)، وأسطر الاختبار المسؤولة، وهل تفشل في كل مرة تحت شرط معروف أم في بعض المرات فقط. ثم اكتب جملة أو جملتين لريم.
- راجع الاختبار بحثاً عن مشكلات لم تُفشل أي تشغيل بعد: المحددات، والتأكيد، والتنظيف بعد الاختبار، والأدلة المحفوظة عند فشل الاختبار.
- اكتب ما تفعله النسخة المستقرة من الاختبار بشكل مختلف، في قائمة مرقّمة أو بلغة Python.
- وزّع الفحوص على الطبقات: أي فحوص القاعدة PR-4 مكانها اختبارات الوحدة أو اختبارات API، وما الذي يبقى في اختبار الواجهة هذا، وفحص واحد على الأقل يبقى يدوياً.
- ردّ على عمر، وبيّن ما يحدث للاختبار حتى يُصلَح، وأين يعمل بعد ذلك.
إظهار الإجابة النموذجية
المجموعات الست. F1–F5: عيب في محدد الاختبار، لا تذبذب. يبحث السطران 26–27 عن النص الإنجليزي الظاهر «Apply»، والواجهة العربية تعرضه «تطبيق»، فيفشل البحث عن الزر في كل تشغيل بالعربية ولا يفشل في أي تشغيل بالإنجليزية؛ ولا يبدو عشوائياً إلا لأن المصفوفة لم تعطِ العربية إلا 5 تشغيلات من أصل 40، موزعة على الهواتف الأربعة. F6–F8: لوحة المفاتيح التي تغطي العنصر التالي. بعد send_keys في السطر 25 تبقى لوحة المفاتيح مفتوحة، وعلى أصغر شاشة في المصفوفة تغطي زر «Apply»، فلا تصل إليه النقرة في السطرين 26–27 ويبقى المجموع «SAR 100.00»؛ ولا يحدث ذلك إلا على الهاتف C، وليس في كل تشغيل عليه. F9–F10: عدم تزامن عولج بانتظار ثابت. ينتظر السطر 28 ثانيتين، بينما استغرق طلب الخصم 2.6 ثانية و3.1 ثانية، فقرأ السطر 29 المجموع قبل أن يتغير.
F11–F12: حالة مشتركة على واجهة خلفية مشتركة. تسجّل الأجزاء الأربعة الدخول بحساب واحد، فأضاف اختبار في جزء آخر صنفين إلى السلة نفسها على الخادم أثناء تشغيل هذا الاختبار (مجموع فرعي قدره 175.00 ريالاً بعد خصم 20% يصبح 140.00 ريالاً)؛ والسطران 14–15 يفترضان سلة لا ينشئها الاختبار أصلاً. F13: حوار نظام. يغطي طلب إذن الإشعارات التطبيق لأن الأذونات لا تُمنح مسبقاً (الأسطر 6–11)، ولا يظهر إلا في تشغيل لم يُجب فيه أي اختبار سابق على الهاتف نفسه عن هذا الطلب. F14: تسرّب الحالة بين الاختبارات في صورة اعتماد على ترتيبها. يعتمد الاختبار على أن test_login سجّل الدخول قبله على الهاتف نفسه (السطر 10 والسطران 14–15)، وفي تلك الليلة وضعت خطة توزيع الأجزاء test_login على هاتف آخر.
متذبذب أم لا: F1–F5 تفشل في كل مرة تكون فيها اللغة عربية، فهي عيب في الاختبار قابل لإعادة الإنتاج. أما F6–F14 فتنجح وتفشل على الشيفرة نفسها بحسب ظروف لا يتحكم فيها الاختبار — لوحة المفاتيح على أصغر شاشة، وزمن الاستجابة، وما تفعله الأجزاء الأخرى، وحالة الجهاز، وخطة توزيع الأجزاء — فهي متذبذبة بالمعنى الذي يقصده الدليل (وحتى على الهاتف C نجحت التشغيلات الإنجليزية الأخرى). ولريم: التشغيلات الفاشلة الأربعة عشر كلها نتائج إيجابية كاذبة (False-positive results)، فمصدر الفشل الاختبار وتهيئته لا عيب في SAVE20. بل تشير الأدلة إلى العكس: نجح الاختبار في 26 تشغيلاً بالإنجليزية وقرأ فيها «SAR 80.00»، وتُظهر فيديوهات F9–F12 الخصم مطبَّقاً (80.00 ريالاً بعد مؤشر التحميل، و140.00 ريالاً على سلة قيمتها 175.00 ريالاً). أما ما لم تفحصه الحزمة الليلية أصلاً فهو الواجهة العربية، لأن أي تشغيل بالعربية لم يتجاوز زر «Apply». إذن يعمل SAVE20 في الواجهة الإنجليزية، على السلة ذات الصنف الواحد وعلى سلة الأصناف الثلاثة في F11–F12؛ أما الواجهة العربية وبقية القاعدة PR-4 فتحتاج إلى فحوص يمكن الوثوق بها (الخطوة 4)، وإلى أن يُصلَح الاختبار يجب التأكد من SAVE20 بالعربية بطريقة أخرى، مثل فحص يدوي على مرشّح الإصدار.
مشكلات لم تُفشل أي تشغيل بعد. الأسطر 20–22: محدد XPath مبني على مواقع العناصر، بطيء على الجوال لأن شجرة العرض كلها يجب أن تُسلسَل، وينكسر بمجرد أن يتغير شريط التبويبات؛ ويحتاج تبويب السلة إلى معرّف. السطر 23: انتظار ثابت ثانٍ، يكلّف ثلاث ثوانٍ في كل تشغيل، ويُفشل السطر 24 في أي ليلة تتأخر فيها السلة في التحميل. السطر 29: معرّف إمكانية الوصول على Android هو وصف المحتوى الذي يقرؤه TalkBack بصوت عالٍ، فيسمع مستخدم TalkBack الاسم التقني cart.total بدلاً من المبلغ؛ أبلِغ عن ذلك كعيب إمكانية وصول في التطبيق، واستخدم معرّف المورد في الاختبار. السطر 30: القيمة المتوقعة هي النص الإنجليزي SAR 80.00، بينما تعرض الواجهة العربية «ر.س» (انظر F1–F5)، فستفشل التشغيلات العربية هنا بمجرد إصلاح المحدد. السطر 31: يُتخطّى driver.quit() كلما رفع سطر قبله استثناءً — بحث فاشل عن عنصر أو تأكيد فاشل — فتبقى الجلسة مفتوحة على هاتف المزرعة حتى ينهيها Appium عند انتهاء مهلة الأوامر. ولا شيء ينظّف البيانات أيضاً: لا يزيل الاختبار SAVE20 ولا الصنف من سلة الحساب المشترك على الخادم، فيرث أي اختبار تالٍ يسجّل الدخول بهذا الحساب ما تركه هذا الاختبار. ولا يحفظ الاختبار شيئاً خاصاً به عند الفشل: لا لقطة شاشة للخطوة الفاشلة، ولا مصدر الصفحة (Page source)، ولا سجل الجهاز، ولا JUnit XML لخادم CI؛ لا شيء سوى فيديو المزرعة.
النسخة المستقرة من الاختبار. أولاً: ينشئ الاختبار ما يحتاجه بنفسه: مستخدم اختبار فريد لكل تشغيل، وسلة مُهيّأة عبر بيئة الاختبار (صنف واحد بسعر 100.00 ريال)، وتسجيل دخول في تهيئة الاختبار نفسه. وتُحذف "appium:noReset": True فلا ترث أي جلسة بيانات تطبيق من اختبار آخر، ولا يعود الاختبار معتمداً على test_login ولا على خطة توزيع الأجزاء. ثانياً: تمنح "appium:autoGrantPermissions": True الأذونات مسبقاً كما في مثال الدليل (ويتجاهلها مشغّل UiAutomator2 ما دامت noReset مفعّلة، وهذا سبب آخر لحذف السطر 10)، ويُخصَّص لطلب إذن الإشعارات اختبار مستقل. ثالثاً: معرّفات موارد يُتفق عليها مع المطورين، مثل com.example.grocer:id/tab_cart وcom.example.grocer:id/promo_apply وcom.example.grocer:id/cart_total، ويُبحث عنها بـ AppiumBy.ID؛ وعلى iOS تأخذ العناصر نفسها قيماً لـ accessibilityIdentifier مثل cart.total، وهو معرّف لا يقرؤه VoiceOver. ويُحذف وصف المحتوى cart.total من تطبيق Android فيقرأ TalkBack المبلغ نفسه، ويدخل «وجود معرّفات الاختبار» في تعريف الإنجاز (Definition of done). رابعاً: لا time.sleep، بل ينتظر WebDriverWait الشروط: ظهور حقل الخصم، وقابلية «Apply» للنقر، وعرض المجموع للقيمة المتوقعة (EC.text_to_be_present_in_element بمهلة نحو 10–15 ثانية). خامساً: تُغلَق لوحة المفاتيح بـ driver.hide_keyboard() قبل النقر على «Apply». سادساً: يقارن التأكيد المبلغ بوصفه رقماً، أو النص المتوقع للغة التشغيل. سابعاً: ينتقل driver.quit() إلى كتلة finally كما في الدليل، وعند الفشل يحفظ الاختبار لقطة شاشة ومصدر الصفحة وسجل الجهاز، ويتلقى خادم CI ملف JUnit XML (pytest --junitxml=results.xml). ثامناً: تُضبط إعدادات مقياس الحركة الثلاثة على هواتف المزرعة على 0.
الطبقات. حساب خصم 20% قاعدة عمل، فمكان حالاتها تحت الواجهة: اختبارات وحدة لحساب الخصم (20% من المجموع الفرعي، والتقريب إلى 0.01 — لا تحدد PR-4 طريقة التقريب، فاسأل ريم)، واختبارات API لنقطة نهاية الخصم (رمز صالح، ورمز غير معروف، ورمز فارغ، مع الاستجابة المتوقعة لكل منها). تعمل هذه الاختبارات في ثوانٍ مع كل طلب دمج (Pull request) ولا تحتاج إلى جهاز. ويحتفظ اختبار الواجهة بالربط: على شاشة السلة يحدّث الرمزُ الصالح المجموعَ المعروض، ويمكن أن يتحقق اختبار واجهة ثانٍ من أن الرمز غير المعروف يعرض رسالة الخطأ الخاصة به. وتبقى اللغتان في المصفوفة، فالتشغيلات العربية هي التي كشفت مشكلتي المحدد والتأكيد. ويبقى يدوياً: جلسة مع TalkBack على شاشة السلة بعد إصلاح وصف المحتوى، والتحقق على الهاتف C بحجم خط كبير من أن المستخدم يستطيع الوصول إلى «Apply» ولوحة المفاتيح مفتوحة.
الرد على عمر. إعادة التشغيل التلقائية تجعل النتيجة الليلية خضراء دون إصلاح أي شيء: ستظل F1–F5 تفشل في كل تشغيل بالعربية، وفي التشغيلات الأخرى تُخفي إعادة التشغيل السبب، وتُخفي بالقدر نفسه أي عيب متقطع حقيقي في خدمة الخصم. أعِد المحاولة على مستوى البنية التحتية فقط (هاتف مزرعة يفشل في فحص الصحة، أو جلسة لا تبدأ)، ولا تستخدمها أبداً لإخفاء إخفاق. وإلى أن يُصلَح الاختبار احجره: يخرج من النتيجة التي تقرر هل الحزمة الليلية خضراء، ويستمر في العمل، ويُتابَع بمالك وموعد. وبعد الإصلاح شغّله مرات متكررة على المصفوفة كلها، باللغتين وعلى الهاتف C، قبل أن يعود إلى بوابة الجودة. بعد ذلك مكانه التشغيل الليلي على المزرعة وتشغيل مرشّح الإصدار على هواتف الطبقتين 1 و2 (Tier 1 وTier 2)، وبما أنه جزء من رحلة إتمام الشراء فيمكن أن يدخل حزمة دخان الواجهة التي تعمل على محاكي Android واحد مع كل طلب دمج.
إن اختلفت إجابتك فراجع كل نقطة بسؤالين يلخّصان نصيحة الدليل: هل ينشئ الاختبار بنفسه كل ما يعتمد عليه (الحساب، والسلة، والأذونات، وإعدادات الجهاز)؟ وهل ينتظر شرطاً لا وقتاً؟ والإصلاح المختلف الذي يزيل السبب نفسه مقبول، مثل خادم وهمي يعيد استجابة الخصم في اختبار الواجهة، مع تغطية نقطة النهاية الحقيقية باختبارات API.
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.
جزء من مسار تعلم ضمان الجودة
قبل هذا الموضوع
الموضوعات التالية
الأدوات المذكورة في هذا الدليل
ولّد استجابات API وهمية كاملة (الحالة والترويسات وجسم JSON) لحالات النجاح والقوائم والتفاصيل والأخطاء والنتائج الفارغة.
احصل على نقطة نهاية عامة تعيد أي رمز حالة HTTP، مع تأخير اختياري وجسم JSON مخصّص، لاختبار معالجة الأخطاء في العميل.
ولّد استجابات خطأ منظّمة (التحقق والمصادقة وعدم العثور وتحديد المعدّل وخطأ الخادم) بالصيغ الشائعة.
ولّد سجلات مستخدمين وهمية (الاسم واسم المستخدم والبريد الإلكتروني وكلمة المرور وبذرة الصورة الرمزية والدور) لحسابات الاختبار.
مصنع متكامل لبيانات الاختبار الاصطناعية: مستخدمون وعملاء وشركات وعناوين وأرقام وتواريخ وسلاسل نصية بصيغة JSON أو CSV أو SQL أو XML.
ولّد أصغر مجموعة اختبارات زوجية (all-pairs) من المعاملات وقيمها.
تحقّق من استجابات API وفق JSON Schema أو مخطط استجابة OpenAPI.