آزمونهای 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 مفید باشد.