تست های خود را همگام کنید

تست‌های Compose به طور پیش‌فرض با رابط کاربری شما همگام‌سازی می‌شوند. وقتی یک assertion یا action را با 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() یک assertion یا action را فراخوانی می‌کنید، تست شما با رابط کاربری Compose هماهنگ می‌شود. در برخی موارد، ممکن است بخواهید این هماهنگ‌سازی را متوقف کنید و خودتان ساعت را کنترل کنید. به عنوان مثال، می‌توانید زمان را کنترل کنید تا در نقطه‌ای که رابط کاربری هنوز مشغول است، از یک انیمیشن اسکرین‌شات‌های دقیقی بگیرید. برای غیرفعال کردن هماهنگ‌سازی خودکار، ویژگی autoAdvance را در mainClock روی false تنظیم کنید:

composeTestRule.mainClock.autoAdvance = false

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

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

منابع بلااستفاده

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

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

این API بسیار شبیه به Idling Resources در 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 ) همگام‌سازی‌های ضمنی را فعال می‌کنند که هنگام کنترل دستی ساعت، اضافی هستند، بنابراین دور زدن آنها به طور قابل توجهی زمان اجرای تست شما را سرعت می‌بخشد.

دستورالعمل‌های استفاده

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

پیش از این، تست Compose به شدت یک مدل دو نخی را اعمال می‌کرد: اجرای تست در یک نخ تست پس‌زمینه رخ می‌داد، در حالی که به‌روزرسانی‌های رابط کاربری در نخ اصلی اتفاق می‌افتاد. فراخوانی متدهای همگام‌سازی مانند waitForIdle یا runOnIdle از نخ اصلی (برای مثال، درون یک بلوک runOnUiThread ) یک IllegalStateException ایجاد می‌کرد زیرا این چارچوب، بررسی‌های سختگیرانه‌ای را برای جلوگیری از همگام‌سازی نخ اصلی اعمال می‌کرد.

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

چه زمانی از همگام‌سازی نخ اصلی استفاده کنیم؟

اگرچه نگه‌داشتن تست‌ها روی نخ پس‌زمینه همچنان استاندارد تست‌های Compose خالص است، اما همگام‌سازی نخ اصلی در چند سناریوی خاص بسیار سودمند است:

  • قابلیت همکاری پیچیده View : هنگام آزمایش رابط‌های کاربری ترکیبی که شامل Compose و Viewهای قدیمی اندروید هستند، دستکاری Viewها اغلب نیاز به اجرا در thread اصلی دارد. اکنون می‌توانید با Viewها تعامل داشته باشید و به صورت متوالی و بدون تغییر مداوم زمینه‌های thread، روی گره‌های 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")
    }
}

منتظر شرایط باشید

هر شرطی که به کار خارجی بستگی دارد، مانند بارگذاری داده‌ها یا اندازه‌گیری یا ترسیم اندروید (یعنی اندازه‌گیری یا ترسیم خارج از Compose)، باید از یک مفهوم کلی‌تر مانند waitUntil() استفاده کند:

composeTestRule.waitUntil(timeoutMs) { condition }

همچنین می‌توانید از هر یک از کمکی‌های waitUntil استفاده کنید:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

منابع اضافی

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