نوشتن 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های Compose test v2 ComposeUiTestConfig برای سفارشی‌سازی محیط تست استفاده می‌کنند. APIهایی که توابع راه‌اندازی برای تست‌ها ایجاد می‌کنند، مانند createComposeRule ، runComposeUiTest و سایر APIهای مرتبط، ComposeUiTestConfig می‌پذیرند. این شیء پیکربندی، APIهای مرتبط با محیط مانند effectContext ، runTestContext و testTimeout را در یک شیء واحد تجمیع می‌کند.

مدل پیکربندی همچنین inputMode مدیریت می‌کند. APIهای Compose test v2 به طور پیش‌فرض InputMode.Touch را در شروع هر تست اجرا می‌کنند تا قطعیت را تضمین کرده و از نشت وضعیت حالت ورودی بین تست‌ها جلوگیری کنند.

ComposeUiTestConfig بخشی از APIهای Compose Test نسخه ۲ است که به طور پیش‌فرض از StandardTestDispatcher استفاده می‌کنند. اگر تست‌های شما از APIهای نسخه ۱ استفاده می‌کنند، قبل از اتخاذ ComposeUiTestConfig به Migrate to v2 testing APIs مراجعه کنید.

مهاجرت به ComposeUiTestConfig

در داخل overloadهای مربوط به ایجاد توابع setup در تست‌ها، چندین overload که پارامترهای پیکربندی منفرد -- مانند effectContext ، runTestContext یا testTimeout -- را می‌پذیرند، منسوخ شده‌اند. تست‌های خود را به‌روزرسانی کنید تا به جای آن ComposeUiTestConfig استفاده کنند، همانطور که در مثال زیر نشان داده شده است:

حالت ورودی پیش‌فرض

اگر تست‌ها قبل از شروع تست به حالت‌های ورودی غیر لمسی که از طریق APIهای ابزار دقیق پیکربندی شده‌اند، متکی باشند، ممکن است در طول مهاجرت با شکست مواجه شوند. در داخل توابع راه‌اندازی تست‌ها، سیستم به طور پیش‌فرض InputMode.Touch را در شروع هر تست برای قطعیت بیشتر و جلوگیری از نشت وضعیت، با نادیده گرفتن وضعیت دستگاه محیطی و تنظیمات پیش از تست، اعمال می‌کند.

برای حل این مشکل، حالت ورودی مورد نیاز را در ComposeUiTestConfig مشخص کنید:

class FocusTest {
    @get:Rule
    val rule = createComposeRule(
        config = ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    )

    @Test
    fun testFocus() {}
}

برای پیکربندی حالت ورودی برای موارد آزمایشی منفرد به جای کل کلاس آزمایشی، ComposeUiTestConfig به runComposeUiTest ارسال کنید:

class FocusTest {
    @Test
    fun testTouchMode() = runComposeUiTest {
        // Runs with the default InputMode.Touch
    }

    @Test
    fun testKeyboardMode() = runComposeUiTest(
        ComposeUiTestConfig(inputMode = InputMode.Keyboard)
    ) {
        // Runs with InputMode.Keyboard
    }
}

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

مهاجرت به 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()
    }
}