مزامنة الاختبارات

تتم مزامنة اختبارات Compose تلقائيًا مع واجهة المستخدم. عند استدعاء تأكيد أو إجراء باستخدام ComposeTestRule، تتم مزامنة الاختبار مسبقًا، بانتظار أن تصبح شجرة واجهة المستخدم غير نشطة.

في العادة، ليس عليك اتخاذ أي إجراء. ومع ذلك، تجدر الإشارة إلى بعض الحالات الاستثنائية التي يجب عليك معرفتها.

عند مزامنة اختبار، يتم تقديم تطبيق Compose في الوقت باستخدام ساعة افتراضية. هذا يعني أنّ اختبارات Compose لا تعمل في الوقت الفعلي، وبالتالي يمكن تنفيذها بأقصى سرعة ممكنة.

ومع ذلك، إذا لم تستخدِم الطرق التي تتم فيها مزامنة اختباراتك، لن تتم إعادة التركيب، وسيبدو أنّ واجهة المستخدم متوقّفة مؤقتًا.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

يُرجى العِلم أنّ هذا الشرط ينطبق فقط على التسلسلات الهرمية في Compose وليس على بقية التطبيق.

إيقاف المزامنة التلقائية

عند طلب تأكيد أو إجراء من خلال ComposeTestRule، مثل assertExists()، تتم مزامنة اختبارك مع واجهة مستخدم Compose. في بعض الحالات، قد تحتاج إلى إيقاف هذه المزامنة والتحكّم في الساعة بنفسك. على سبيل المثال، يمكنك ضبط الوقت لالتقاط لقطات شاشة دقيقة للرسوم المتحركة، بينما تكون واجهة المستخدم لا تزال مشغولة. لإيقاف المزامنة التلقائية، اضبط السمة autoAdvance في mainClock على false:

composeTestRule.mainClock.autoAdvance = false

بعد ذلك، عليك عادةً تقديم الوقت بنفسك. يمكنك تقديم الوقت بمقدار إطار واحد تمامًا باستخدام advanceTimeByFrame()، أو لفترة زمنية محددة باستخدام advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

الموارد غير المستخدَمة

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

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

تشبه واجهة برمجة التطبيقات هذه إلى حد كبير مصادر عدم النشاط في Espresso للإشارة إلى ما إذا كان العنصر قيد الاختبار في وضع الخمول أو مشغولاً. استخدِم قاعدة اختبار Compose لتسجيل عملية تنفيذ IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

المزامنة اليدوية

في بعض الحالات، عليك مزامنة واجهة مستخدم Compose مع أجزاء أخرى من الاختبار أو التطبيق الذي تختبره.

تنتظر الدالة waitForIdle() حتى يصبح Compose في وضع عدم النشاط، ولكنّ الدالة تعتمد على السمة autoAdvance:

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

يُرجى العِلم أنّه في كلتا الحالتين، ينتظر waitForIdle() أيضًا عمليات الرسم والتنسيق المعلّقة.

يمكنك أيضًا تقديم الوقت حتى يتم استيفاء شرط معيّن باستخدام advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

يُرجى العِلم أنّ الشرط المحدّد يجب أن يتحقّق من الحالة التي يمكن أن تتأثر بهذه الساعة (لا تعمل إلا مع حالة Compose).

تحسين اختبارات الصور المتحركة

عند اختبار الصور المتحركة العالية الدقة، غالبًا ما تحتاج إلى إيقاف ميزة "الانتقال التلقائي" والتنقّل يدويًا بين اللقطات لتأكيد حالات واجهة المستخدم المتوسطة. بالنسبة إلى هذه التكرارات الحلقية المحدّدة لكل لقطة، استخدِم طريقة runWithoutImplicitWait لتنفيذ التأكيدات. تؤدي طلبات البحث العادية عن العُقد (مثل onNodeWithTag أو fetchSemanticsNode) إلى عمليات مزامنة ضمنية زائدة عن الحاجة عند التحكّم في الساعة يدويًا، لذا فإنّ تجاوزها يؤدي إلى تسريع أوقات تشغيل الاختبار بشكل كبير.

إرشادات الاستخدام

  • إدارة الساعة يدويًا: استخدِم واجهة برمجة التطبيقات هذه عندما يتم ضبط mainClock.autoAdvance على false وتكون واجهة المستخدم في حالة معروفة ومستقرة للإطار الحالي.
  • تنفيذ سلسلة واجهة المستخدم: لضمان استقرار شجرة واجهة المستخدم، يمكنك استدعاء runWithoutImplicitWait في سلسلة واجهة المستخدم، مثل runOnUiThread. ويؤدي تشغيلها خارج سلسلة واجهة المستخدم إلى تعرُّض الاختبار لحالات السباق وقراءات الحالة القديمة.
  • تأكيدات للقراءة فقط: يجب أن يحتوي المقتطف على تأكيدات للقراءة فقط. يجب تنفيذ أي إجراءات تغيّر الحالة خارج هذه الكتلة.

مثال

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}

مزامنة سلسلة التعليمات الرئيسية

يتيح اختبار Compose الآن مزامنة سلسلة التعليمات الرئيسية، ما يسمح لك باستدعاء waitForIdle بأمان، وبالتالي إجراءات واجهة مستخدم Compose والتحقّقات، مباشرةً من سلسلة التعليمات الرئيسية.

في السابق، كان اختبار Compose يفرض نموذجًا صارمًا يتألف من سلسلتَي عمليات: يتم تنفيذ الاختبار على سلسلة عمليات اختبار في الخلفية، بينما تحدث تحديثات واجهة المستخدم على سلسلة العمليات الرئيسية. إنّ استدعاء طرق المزامنة، مثل waitForIdle أو runOnIdle، من سلسلة التعليمات الرئيسية (على سبيل المثال، داخل كتلة runOnUiThread) سيؤدي إلى ظهور الخطأ IllegalStateException لأنّ إطار العمل يفرض عمليات تحقّق صارمة من سلاسل التعليمات لمنع مزامنة سلسلة التعليمات الرئيسية.

عند تفعيل ميزة مزامنة سلسلة التعليمات الرئيسية، يمكن لإطار عمل اختبار Compose الآن تقديم الساعة ومعالجة العمل في انتظار المراجعة حتى عند إجراء مكالمات حظر على سلسلة التعليمات الرئيسية.

حالات استخدام مزامنة سلسلة المحادثات الرئيسية

على الرغم من أنّ إجراء الاختبارات في سلسلة التعليمات في الخلفية يظل هو المعيار الأساسي لاختبارات Compose البحتة، إلا أنّ مزامنة سلسلة التعليمات الرئيسية تكون مفيدة للغاية في بعض السيناريوهات المحدّدة:

  • إمكانية التشغيل التفاعلي المعقّد مع View: عند اختبار واجهات مستخدم مختلطة تحتوي على كل من Compose وAndroid Views القديمة، غالبًا ما تتطلّب معالجة Views التشغيل على سلسلة التعليمات الرئيسية. يمكنك الآن التفاعل مع طرق العرض والتحقّق من عُقد Compose بشكل تسلسلي بدون تبديل سياقات سلاسل المحادثات باستمرار.
  • تغييرات الحالة المتزامنة: إذا كانت البنية تعتمد بشكل صارم على عناصر الاحتفاظ بالحالة المرتبطة بسلسلة التعليمات الرئيسية، يمكنك الآن تغيير الحالة والانتظار فورًا حتى تستقر واجهة مستخدم Compose بدون مغادرة سلسلة التعليمات الرئيسية.
  • أدوات تشغيل الاختبارات المخصّصة: إذا كنت تنشئ بنية أساسية مخصّصة للاختبار أو تستخدم بيئات يتم فيها تشغيل أداة الاختبار بطبيعتها على سلسلة التعليمات الرئيسية، يتم الآن تنفيذ اختبارات Compose بشكل سلس بدون الحاجة إلى تفويض سلسلة التعليمات في الخلفية.

مثال

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

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

عند تفعيل ميزة مزامنة سلسلة التعليمات الرئيسية، يمكن تنفيذ تأكيدات Compose وهيكليات طرق العرض في المجموعة نفسها:

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

انتظار الشروط

يجب أن يستخدم أي شرط يعتمد على عمل خارجي، مثل تحميل البيانات أو قياس Android أو رسمه (أي القياس أو الرسم خارج Compose)، مفهومًا أكثر عمومية مثل waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

يمكنك أيضًا استخدام أي من مساعدي waitUntil:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

موارد إضافية

  • اختبار التطبيقات على Android: توفّر الصفحة المقصودة الرئيسية لاختبار تطبيقات Android نظرة أوسع على أساسيات الاختبار وأساليبه.
  • أساسيات الاختبار: يمكنك الاطّلاع على مزيد من المعلومات حول المفاهيم الأساسية لاختبار تطبيق Android.
  • الاختبارات المحلية: يمكنك إجراء بعض الاختبارات محليًا على محطة العمل الخاصة بك.
  • اختبارات لقياس حالة التطبيق: من الممارسات الجيدة أيضًا إجراء اختبارات لقياس حالة التطبيق. أي الاختبارات التي يتم إجراؤها مباشرةً على الجهاز.
  • التكامل المستمر: يتيح لك التكامل المستمر دمج اختباراتك في مسار النشر.
  • اختبار أحجام الشاشات المختلفة: نظرًا لتوفّر العديد من الأجهزة للمستخدمين، يجب إجراء اختبارات لأحجام الشاشات المختلفة.
  • Espresso: على الرغم من أنّ Espresso مُصمَّمة لواجهات المستخدم المستندة إلى العرض، إلا أنّ معرفة أدوات Espresso يمكن أن تكون مفيدة في بعض جوانب اختبار Compose.