مهاجرت به APIهای تست نسخه ۲

نسخه‌های v2 از APIهای تست Compose ( createComposeRule ، createAndroidComposeRule ، runComposeUiTest ، runAndroidComposeUiTest و غیره) اکنون برای بهبود کنترل بر اجرای کوروتین در دسترس هستند. این به‌روزرسانی کل سطح API را کپی نمی‌کند؛ فقط APIهایی که محیط تست را ایجاد می‌کنند به‌روزرسانی شده‌اند.

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

این تغییرات در androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+ و androidx.compose.ui:ui-test:1.11.0-alpha03+ لحاظ شده‌اند.

در حالی که APIهای نسخه ۱ به UnconfinedTestDispatcher متکی بودند، APIهای نسخه ۲ به طور پیش‌فرض از StandardTestDispatcher برای اجرای ترکیب استفاده می‌کنند. این تغییر، رفتار تست Compose را با APIهای استاندارد runTest همسو می‌کند و کنترل صریحی بر ترتیب اجرای کوروتین ارائه می‌دهد.

نگاشت‌های API

هنگام ارتقا به APIهای نسخه ۲، معمولاً می‌توانید از Find + Replace برای به‌روزرسانی فایل‌های ورودی بسته و تطبیق با تغییرات جدید dispatcher استفاده کنید.

روش دیگر، از Gemini بخواهید که با استفاده از اعلان زیر، مهاجرت به نسخه ۲ از APIهای تست Compose را انجام دهد:

مهاجرت از APIهای تست نسخه ۱ به APIهای تست نسخه ۲

این اعلان از این راهنما برای مهاجرت به APIهای تست نسخه ۲ استفاده خواهد کرد.

Migrate to Compose testing v2 APIs using the official
migration guide.

استفاده از پیام‌های هوش مصنوعی

دستورات هوش مصنوعی برای استفاده در Gemini در اندروید استودیو در نظر گرفته شده‌اند.

برای اطلاعات بیشتر در مورد Gemini در استودیو به اینجا مراجعه کنید: https://developer.android.com/studio/gemini/overview

خود را به اشتراک بگذارید

از جدول زیر برای نگاشت APIهای نسخه ۱ منسوخ‌شده به جایگزین‌های نسخه ۲ آنها استفاده کنید:

منسوخ شده (نسخه ۱)

جایگزینی (نسخه ۲)

androidx.compose.ui.test.junit4.createComposeRule

androidx.compose.ui.test.junit4.v2.createComposeRule

androidx.compose.ui.test.junit4.createAndroidComposeRule

androidx.compose.ui.test.junit4.v2.createAndroidComposeRule

androidx.compose.ui.test.junit4.createEmptyComposeRule

androidx.compose.ui.test.junit4.v2.createEmptyComposeRule

androidx.compose.ui.test.junit4.AndroidComposeTestRule

androidx.compose.ui.test.junit4.v2.AndroidComposeTestRule

androidx.compose.ui.test.runComposeUiTest

androidx.compose.ui.test.v2.runComposeUiTest

androidx.compose.ui.test.runAndroidComposeUiTest

androidx.compose.ui.test.v2.runAndroidComposeUiTest

androidx.compose.ui.test.runEmptyComposeUiTest

androidx.compose.ui.test.v2.runEmptyComposeUiTest

androidx.compose.ui.test.AndroidComposeUiTestEnvironment

androidx.compose.ui.test.v2.AndroidComposeUiTestEnvironment

سازگاری معکوس و استثنائات

APIهای نسخه ۱ موجود اکنون منسوخ شده‌اند، اما همچنان از UnconfinedTestDispatcher برای حفظ رفتار موجود و جلوگیری از تغییرات مخرب استفاده می‌شود.

مورد زیر تنها استثنایی است که در آن رفتار پیش‌فرض تغییر کرده است:

توزیع‌کننده‌ی تست پیش‌فرض مورد استفاده برای اجرای ترکیب در کلاس AndroidComposeUiTestEnvironment از UnconfinedTestDispatcher به StandardTestDispatcher تغییر یافته است. این موضوع مواردی را تحت تأثیر قرار می‌دهد که شما با استفاده از سازنده یا زیرکلاس AndroidComposeUiTestEnvironment یک نمونه ایجاد می‌کنید و آن سازنده را فراخوانی می‌کنید.

تغییر کلیدی: تأثیر بر اجرای کوروتین

تفاوت اصلی بین نسخه ۱ و نسخه ۲ رابط‌های برنامه‌نویسی کاربردی (API) در نحوه ارسال کوروتین‌ها است:

  • APIهای نسخه ۱ ( UnconfinedTestDispatcher ): وقتی یک کوروتین اجرا می‌شد، بلافاصله روی نخ فعلی اجرا می‌شد و اغلب قبل از اجرای خط بعدی کد تست، به پایان می‌رسید. برخلاف رفتار در محیط عملیاتی، این اجرای فوری می‌توانست سهواً مشکلات زمان‌بندی واقعی یا شرایط رقابتی را که در یک برنامه زنده رخ می‌دهد، بپوشاند .
  • APIهای نسخه ۲ ( StandardTestDispatcher ): وقتی یک کوروتین اجرا می‌شود، در صف قرار می‌گیرد و تا زمانی که تست به طور صریح ساعت مجازی را جلو نبرد، اجرا نمی‌شود. APIهای تست استاندارد Compose (مانند waitForIdle() ) از قبل این همگام‌سازی را مدیریت می‌کنند، بنابراین اکثر تست‌هایی که به این APIهای استاندارد متکی هستند، باید بدون هیچ تغییری به کار خود ادامه دهند.

خرابی‌های رایج و نحوه رفع آنها

اگر تست‌های شما پس از ارتقا به نسخه ۲ با شکست مواجه شوند، احتمالاً الگوی زیر را نشان می‌دهند:

  • شکست : شما یک وظیفه را اجرا می‌کنید (برای مثال، یک ViewModel داده‌ها را بارگذاری می‌کند)، اما درخواست شما بلافاصله با شکست مواجه می‌شود زیرا داده‌ها هنوز در حالت "بارگذاری" هستند.
  • علت : با API های نسخه ۲، کوروتین‌ها به جای اینکه بلافاصله اجرا شوند، در صف قرار می‌گیرند. وظیفه در صف قرار گرفت اما قبل از بررسی نتیجه، هرگز اجرا نشد.
  • راه حل : زمان را به طور صریح جلو ببرید. شما باید به طور صریح به توزیع کننده v2 بگویید که چه زمانی کار را انجام دهد.

رویکرد قبلی

در نسخه ۱، وظیفه بلافاصله اجرا و پایان یافت. در نسخه ۲، کد زیر با شکست مواجه می‌شود زیرا loadData() هنوز اجرا نشده است.

// In v1, this launched and finished immediately.
viewModel.loadData()

// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)

برای اجرای وظایف در صف انتظار قبل از assertion، waitForIdle یا runOnIdle استفاده کنید.

گزینه ۱ : استفاده از waitForIdle ساعت را تا زمانی که رابط کاربری بیکار شود، جلو می‌برد و تأیید می‌کند که کوروتین اجرا شده است.

viewModel.loadData()

// Explicitly run all queued tasks
composeTestRule.waitForIdle()

assertEquals(Success, viewModel.state.value)

گزینه ۲ : استفاده از runOnIdle بلوک کد را پس از بیکار شدن رابط کاربری، روی نخ رابط کاربری اجرا می‌کند.

viewModel.loadData()

// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
    assertEquals(Success, viewModel.state.value)
}

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

در سناریوهایی که شامل همگام‌سازی دستی هستند، مانند زمانی که پیشرفت خودکار غیرفعال است، راه‌اندازی یک کوروتین منجر به اجرای فوری نمی‌شود زیرا ساعت آزمایشی متوقف شده است. برای اجرای کوروتین‌ها در صف بدون پیشرفت ساعت مجازی، از API runCurrent() استفاده کنید. این API وظایفی را که برای زمان مجازی فعلی برنامه‌ریزی شده‌اند، اجرا می‌کند.

composeTestRule.mainClock.scheduler.runCurrent()

برخلاف waitForIdle() که ساعت آزمایشی را تا زمان تثبیت رابط کاربری جلو می‌برد، runCurrent() وظایف در حال انتظار را اجرا می‌کند و در عین حال زمان مجازی فعلی را حفظ می‌کند. این رفتار امکان تأیید حالت‌های میانی را فراهم می‌کند که در صورت جلو بردن ساعت به حالت بیکار، از آنها صرف نظر می‌شد.

زمان‌بند تست زیربنایی مورد استفاده در محیط تست، در معرض دید قرار می‌گیرد. این زمان‌بند می‌تواند همراه با API runTest کاتلین برای همگام‌سازی ساعت تست استفاده شود.

مهاجرت به runComposeUiTest

اگر از APIهای تست Compose در کنار API مربوط به Kotlin runTest استفاده می‌کنید، اکیداً توصیه می‌شود که به runComposeUiTest تغییر دهید.

رویکرد قبلی

استفاده از createComposeRule همراه با runTest دو ساعت جداگانه ایجاد می‌کند: یکی برای Compose و دیگری برای محدوده کوروتین تست. این پیکربندی می‌تواند شما را مجبور کند که زمان‌بند تست را به صورت دستی همگام‌سازی کنید.

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testWithCoroutines() {
    composeTestRule.setContent {
        var status by remember { mutableStateOf("Loading...") }
        LaunchedEffect(Unit) {
            delay(1000)
            status = "Done!"
        }
        Text(text = status)
    }

    // NOT RECOMMENDED
    // Fails: runTest creates a new, separate scheduler.
    // Advancing time here does NOT advance the compose clock.
    // To fix this without migrating, you would need to share the scheduler
    // by passing 'composeTestRule.mainClock.scheduler' to runTest.
    runTest {
        composeTestRule.onNodeWithText("Loading...").assertIsDisplayed()
        advanceTimeBy(1000)
        composeTestRule.onNodeWithText("Done!").assertIsDisplayed()
    }
}

API runComposeUiTest به طور خودکار بلوک تست شما را در محدوده runTest خودش اجرا می‌کند. ساعت تست با محیط Compose هماهنگ شده است، بنابراین دیگر نیازی به مدیریت دستی زمان‌بندی ندارید.

    @Test
    fun testWithCoroutines() = runComposeUiTest {
        setContent {
            var status by remember { mutableStateOf("Loading...") }
            LaunchedEffect(Unit) {
                delay(1000)
                status = "Done!"
            }
            Text(text = status)
        }

        onNodeWithText("Loading...").assertIsDisplayed()
        mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */)
        onNodeWithText("Done!").assertIsDisplayed()
    }
}