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

آزمون‌های Compose به‌طور پیش‌فرض با میانای کاربر شما همگام‌سازی می‌شوند. وقتی یک ادعا یا کنشی را با ComposeTestRule فراخوانی می‌کنید، آزمایش ازقبل همگام‌سازی می‌شود و تا زمانی که درخت واسط کاربر غیرفعال شود منتظر می‌ماند.

معمولاً لازم نیست اقدامی انجام دهید. بااین‌حال، چند مورد لبه‌ای وجود دارد که باید درباره آن‌ها بدانید.

وقتی آزمایشی همگام‌سازی می‌شود، برنامه «نگارش» شما بااستفاده از ساعت مجازی در زمان جلو می‌رود. این یعنی آزمایش‌های «نوشتن» در زمان واقعی اجرا نمی‌شوند، بنابراین می‌توانند در سریع‌ترین زمان ممکن قبول شوند.

بااین‌حال، اگر از روش‌هایی که آزمون‌هایتان را همگام‌سازی می‌کنند استفاده نکنید، بازسازی‌ای انجام نخواهد شد و به‌نظر می‌رسد واسط کاربر موقتاً متوقف شده است.

@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()
}

توجه داشته باشید که این الزام فقط برای سلسله‌مراتب «نوشتن» اعمال می‌شود و نه برای بقیه برنامه.

غیرفعال کردن همگام‌سازی خودکار

وقتی ازطریق ComposeTestRule مثل assertExists() ادعا یا کنشی را فراخوانی می‌کنید، آزمایشتان با «واسط کاربر نوشتن» هم‌زمان می‌شود. در برخی موارد ممکن است بخواهید این هم‌زمان‌سازی را متوقف کنید و خودتان ساعت را کنترل کنید. برای مثال، می‌توانید زمان را کنترل کنید تا نماگرفت‌های دقیقی از پویانمایی در نقطه‌ای که رابط کاربری هنوز مشغول است بگیرید. برای غیرفعال کردن همگام‌سازی خودکار، ویژگی autoAdvance را در mainClock روی false تنظیم کنید:

composeTestRule.mainClock.autoAdvance = false

معمولاً خودتان زمان را جلو می‌برید. می‌توانید دقیقاً یک قاب با advanceTimeByFrame() یا با مدت زمان مشخص با advanceTimeBy() جلو بروید:

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

منابع بدون فعالیت

‫Compose می‌تواند آزمایش‌ها و میانای کاربر را هم‌زمان‌سازی کند تا هر کنش و ادعایی در حالت بیکاری انجام شود و درصورت نیاز، ساعت را به‌جلو ببرد یا منتظر بماند. بااین‌حال، برخی‌از عملیات ناهم‌زمان که نتایج آن‌ها بر وضعیت واسط کاربر تأثیر می‌گذارد می‌توانند در پس‌زمینه اجرا شوند درحالی‌که آزمایش از آن‌ها بی‌اطلاع است.

این منابع درانتظار را در آزمایشتان ایجاد و ثبت کنید تا هنگام تصمیم‌گیری درباره اینکه برنامه تحت آزمایش مشغول است یا درانتظار، درنظر گرفته شوند. لازم نیست اقدامی انجام دهید، مگراینکه نیاز داشته باشید منابع درانتظار اضافی را ثبت کنید، برای مثال، اگر کار زمینه‌ای اجرا می‌کنید که با Espresso یا Compose همگام‌سازی نمی‌شود.

این API بسیار شبیه به منابع درانتظار Espresso است تا نشان دهد آیا موضوع تحت آزمایش درانتظار است یا مشغول. از قانون آزمایش «نوشتن» برای ثبت پیاده‌سازی IdlingResource استفاده کنید.

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

همگام‌سازی دستی

در برخی موارد، باید «واسط کاربر Compose» را با بخش‌های دیگر آزمایشتان یا برنامه‌ای که آزمایش می‌کنید همگام‌سازی کنید.

تابع waitForIdle() منتظر می‌ماند تا «نوشتن» غیرفعال شود، اما تابع به دارایی 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 }

توجه داشته باشید که شرط داده‌شده باید وضعیتی را بررسی کند که می‌تواند تحت‌تأثیر این ساعت قرار گیرد (این شرط فقط با وضعیت «نوشتن» کار می‌کند).

بهینه‌سازی آزمایش‌های پویانمایی

هنگام آزمایش پویانمایی‌های با وفاداری بالا، اغلب باید پیشروی خودکار را غیرفعال کنید و به‌صورت دستی از میان قاب‌ها عبور کنید تا وضعیت‌های واسط کاربر میانی را تأیید کنید. برای این حلقه‌های خاص فریم‌به‌فریم، از روش 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 و هم «نماهای Android» قدیمی را دارند، دستکاری «نماها» اغلب نیاز دارد که در رشته اصلی اجرا شود. اکنون می‌توانید با «نماها» تعامل داشته باشید و در گره‌های «نوشتن» به‌صورت متوالی بدون تغییر مداوم بافت رشته ادعا کنید.
  • جهش‌های وضعیت هم‌زمان: اگر معماری شما به نگهدارنده‌های وضعیت کاملاً محدود به رشته اصلی متکی باشد، اکنون می‌توانید وضعیت را جهش دهید و بلافاصله بدون ترک رشته اصلی منتظر بمانید تا «میانای کاربری Compose» مستقر شود.
  • اجراکننده‌های آزمون سفارشی: اگر زیرساخت آزمون سفارشی می‌سازید یا از محیط‌هایی استفاده می‌کنید که در آن‌ها اجراکننده آزمون به‌طور ذاتی در رشته اصلی اجرا می‌شود، آزمون‌های Compose اکنون بدون نیاز به واگذاری رشته پس‌زمینه به‌طور پاک اجرا می‌شوند.

مثال

ازنظر تاریخی، چون همگام‌سازی در رشته اصلی به‌شدت ممنوع بود، توسعه‌دهندگان مجبور بودند بین رشته اجراکننده آزمون پس‌زمینه و رشته UI به‌صورت رفت‌وبرگشتی حرکت کنند که منجر به آزمون‌های گسسته می‌شد:

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