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

تتم مزامنة اختبارات 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 البحتة، إلا أنّ مزامنة سلسلة التعليمات الرئيسية مفيدة جدًا في بعض السيناريوهات المحدّدة:

  • إمكانية التشغيل التفاعلي مع Complex View: عند اختبار واجهات مستخدم مختلطة تحتوي على كل من Compose وAndroid Views القديمة، تتطلّب معالجة عناصر View غالبًا التشغيل على سلسلة التعليمات الرئيسية. يمكنك الآن التفاعل مع طرق العرض والتأكّد من صحة عقد 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 وView في الحظر نفسه:

@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.