همگام‌سازی آزمون‌ها

آزمون‌های 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 UI همگام‌سازی می‌شود. در برخی موارد ممکن است بخواهید این همگام‌سازی را متوقف کنید و خودتان ساعت را کنترل کنید. برای مثال، می‌توانید زمان را کنترل کنید تا نماگرفت‌های دقیقی از پویانمایی در نقطه‌ای که میانای کاربر هنوز مشغول است بگیرید. برای غیرفعال کردن همگام‌سازی خودکار، ویژگی 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 UI را با بخش‌های دیگر آزمایش یا برنامه‌ای که آزمایش می‌کنید همگام‌سازی کنید.

تابع 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 تنظیم شده باشد و واسط کاربر در وضعیت شناخته‌شده و پایداری برای قاب فعلی باشد، از این API استفاده کنید.
  • اجرای رشته میانای کاربر: برای اطمینان از پایداری درخت میانای کاربر، 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 خالص است، همگام‌سازی رشته اصلی در چند سناریو خاص بسیار سودمند است:

  • هم‌کنش‌پذیری نمای پیچیده: هنگام آزمایش واسط‌های کاربر ترکیبی که هم Compose و هم نماهای قدیمی Android را شامل می‌شوند، دستکاری نماها اغلب نیاز به اجرا در رشته اصلی دارد. اکنون می‌توانید با «نماها» تعامل داشته باشید و به‌طور متوالی روی گره‌های Compose ادعا کنید بدون اینکه مدام زمینه‌های رشته را تغییر دهید.
  • جهش‌های حالت همگام: اگر معماری شما به‌شدت به نگه‌دارنده‌های حالت محدود به رشته اصلی متکی است، اکنون می‌توانید حالت را جهش دهید و بلافاصله منتظر بمانید تا Compose UI بدون ترک رشته اصلی مستقر شود.
  • اجراکننده‌های آزمون سفارشی: اگر درحال ساختن زیرساخت آزمایشی سفارشی هستید یا از محیط‌هایی استفاده می‌کنید که اجراکننده آزمون ذاتاً در رشته اصلی اجرا می‌شود، آزمون‌های Compose اکنون به‌طور تمیز و بدون نیاز به واگذاری رشته پس‌زمینه اجرا می‌شوند.

مثال

Historically, because synchronization was strictly prohibited on the main thread, developers had to bounce back-and-forth between the background test runner thread and the UI thread, leading to disjointed tests:

@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 برای واسط‌های کاربر مبتنی بر View طراحی شده است، اما دانش Espresso همچنان می‌تواند برای برخی جنبه‌های آزمایش Compose مفید باشد.