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

تتم مزامنة اختبارات 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).

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

When testing high-fidelity animations, you often need to disable auto-advance and manually step through frames to assert intermediate UI states. For these specific frame-by-frame loops, use the runWithoutImplicitWait method to execute your assertions. Standard node queries (like onNodeWithTag or fetchSemanticsNode) trigger implicit synchronizations that are redundant when you are manually controlling the clock, so bypassing them significantly speeds up your test runtimes.

Usage guidelines

  • Manual clock management: Use this API when mainClock.autoAdvance is set to false and the UI is in a known, stable state for the current frame.
  • UI thread execution: To ensure the stability of the UI tree, call runWithoutImplicitWait on the UI thread, such as with runOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads.
  • Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.

Example

@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 وViews القديمة في 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 و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 helpers:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

موارد إضافية

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