การทดสอบ Compose จะซิงค์กับ UI โดยค่าเริ่มต้น เมื่อเรียกใช้การยืนยันหรือการดำเนินการด้วย ComposeTestRule ระบบจะซิงค์การทดสอบล่วงหน้า โดยรอจนกว่าทรี UI จะไม่มีการใช้งาน
โดยปกติแล้วคุณไม่ต้องดำเนินการใดๆ อย่างไรก็ตาม มีกรณีที่เป็นปัญหาที่สุด ที่คุณควรรู้
เมื่อซิงค์การทดสอบแล้ว แอป Compose จะเลื่อนเวลาไปข้างหน้าโดยใช้ นาฬิกาเสมือน ซึ่งหมายความว่าการทดสอบ Compose จะไม่ทำงานแบบเรียลไทม์ จึงสามารถผ่านการทดสอบได้ โดยเร็วที่สุด
อย่างไรก็ตาม หากคุณไม่ได้ใช้วิธีการที่ซิงค์การทดสอบ จะไม่มีการจัดองค์ประกอบใหม่และ UI จะดูเหมือนหยุดชั่วคราว
@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() การทดสอบจะซิงค์กับ UI ของ Compose ในบางกรณี
คุณอาจต้องการหยุดการซิงค์นี้และควบคุมนาฬิกาด้วยตนเอง เช่น คุณสามารถควบคุมเวลาเพื่อถ่ายภาพหน้าจอของภาพเคลื่อนไหวได้อย่างแม่นยำในจุดที่ UI ยังคงทำงานอยู่ หากต้องการปิดใช้การซิงค์อัตโนมัติ ให้ตั้งค่าพร็อพเพอร์ตี้ autoAdvance ใน mainClock เป็น false ดังนี้
composeTestRule.mainClock.autoAdvance = false
โดยปกติแล้วคุณจะต้องเลื่อนเวลาด้วยตนเอง คุณสามารถเลื่อนไปข้างหน้า 1 เฟรมด้วย advanceTimeByFrame() หรือเลื่อนตามระยะเวลาที่ต้องการด้วย advanceTimeBy() ได้
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
ทรัพยากรที่ไม่ได้ใช้งาน
Compose สามารถซิงค์การทดสอบและ UI เพื่อให้ทุกการดำเนินการและการยืนยันเกิดขึ้นในสถานะไม่มีการใช้งาน โดยรอหรือเลื่อนเวลาตามต้องการ อย่างไรก็ตาม การดำเนินการแบบไม่พร้อมกันบางอย่างซึ่งผลลัพธ์มีผลต่อสถานะ UI สามารถเรียกใช้ใน เบื้องหลังได้โดยที่การทดสอบไม่ทราบถึงการดำเนินการเหล่านั้น
สร้างและลงทะเบียนทรัพยากรที่ไม่มีการใช้งานเหล่านี้ในการทดสอบเพื่อให้ระบบนำไปพิจารณาเมื่อตัดสินว่าแอปที่อยู่ระหว่างทดสอบกำลังทำงานหรือไม่มีการใช้งาน คุณไม่จำเป็นต้องดำเนินการใดๆ เว้นแต่คุณจะต้องลงทะเบียนทรัพยากรที่ไม่ได้ใช้งานเพิ่มเติม เช่น หากคุณเรียกใช้ งานในเบื้องหลังที่ไม่ได้ซิงค์กับ Espresso หรือ Compose
API นี้คล้ายกับ Idling Resources ของ Espresso มาก เพื่อระบุว่า
ออบเจ็กต์ภายใต้การทดสอบไม่ได้ใช้งานหรือกำลังทำงาน ใช้กฎทดสอบการเขียนเพื่อลงทะเบียน
การติดตั้งใช้งาน IdlingResource
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
การซิงค์ด้วยตนเอง
ในบางกรณี คุณต้องซิงค์ UI ของ 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.
โปรดทราบว่าในทั้ง 2 กรณี waitForIdle() จะรอการวาดและการจัดวาง
ที่รอดำเนินการด้วย
นอกจากนี้ คุณยังเลื่อนเวลาไปจนกว่าจะมีเงื่อนไขตรงตามที่กำหนดได้ด้วย advanceTimeUntil()
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
โปรดทราบว่าเงื่อนไขที่ระบุควรตรวจสอบสถานะที่อาจได้รับผลกระทบ จากนาฬิกานี้ (ใช้ได้กับสถานะ Compose เท่านั้น)
เพิ่มประสิทธิภาพการทดสอบภาพเคลื่อนไหว
เมื่อทดสอบภาพเคลื่อนไหวที่มีความเที่ยงตรงสูง คุณมักจะต้องปิดใช้การเปลี่ยนอัตโนมัติ
และเลื่อนผ่านเฟรมด้วยตนเองเพื่อยืนยันสถานะ UI ระหว่างกลาง สำหรับลูปแบบเฟรมต่อเฟรมที่เฉพาะเจาะจงเหล่านี้ ให้ใช้วิธี runWithoutImplicitWait เพื่อ
ดำเนินการยืนยัน การค้นหาโหนดมาตรฐาน (เช่น onNodeWithTag หรือ
fetchSemanticsNode) จะทริกเกอร์การซิงค์โดยนัยซึ่งซ้ำซ้อน
เมื่อคุณควบคุมนาฬิกาด้วยตนเอง ดังนั้นการข้ามการค้นหาเหล่านี้จะช่วยเพิ่มความเร็วในการรันการทดสอบได้อย่างมาก
หลักเกณฑ์การใช้งาน
- การจัดการนาฬิกาด้วยตนเอง: ใช้ API นี้เมื่อตั้งค่า
mainClock.autoAdvanceเป็นfalseและ UI อยู่ในสถานะที่ทราบและเสถียรสำหรับเฟรมปัจจุบัน - การดำเนินการในเธรด UI: เรียกใช้
runWithoutImplicitWaitในเธรด UI เช่น กับrunOnUiThreadเพื่อให้มั่นใจว่าโครงสร้าง UI จะเสถียร การเรียกใช้ในเธรด UI จะทำให้การทดสอบของคุณเสี่ยงต่อสภาพการแข่งขันและการอ่านสถานะที่ล้าสมัย - การยืนยันแบบอ่านอย่างเดียว: บล็อกควรมีการยืนยันแบบอ่านอย่างเดียวอย่างเคร่งครัด การดำเนินการใดๆ ที่เปลี่ยนสถานะควรดำเนินการนอกบล็อกนี้
ตัวอย่าง
@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 ได้อย่างปลอดภัย รวมถึงการดำเนินการและการยืนยัน UI ของ Compose โดยตรงจากเทรดหลัก
ก่อนหน้านี้ การทดสอบ Compose บังคับใช้โมเดล 2 เธรดอย่างเคร่งครัด โดยการดำเนินการทดสอบ
เกิดขึ้นในเธรดทดสอบเบื้องหลัง ขณะที่การอัปเดต UI เกิดขึ้นในเธรดหลัก
การเรียกใช้เมธอดการซิงค์ เช่น waitForIdle หรือ runOnIdle
จากเทรดหลัก (เช่น ภายในบล็อก runOnUiThread) จะทำให้เกิด IllegalStateException เนื่องจากเฟรมเวิร์กบังคับใช้การตรวจสอบเทรดอย่างเข้มงวด
เพื่อป้องกันการซิงค์ในเทรดหลัก
เมื่อเปิดใช้การซิงค์เทรดหลักแล้ว ตอนนี้เฟรมเวิร์กการทดสอบ Compose จะ เลื่อนเวลาและประมวลผลงานที่รอดำเนินการได้ แม้ว่าจะมีการเรียกที่บล็อกใน เทรดหลักก็ตาม
กรณีที่ควรใช้การซิงค์เธรดหลัก
แม้ว่าการทดสอบในเทรดเบื้องหลังจะยังคงเป็นมาตรฐานสำหรับการทดสอบ Compose แบบเพียว แต่การซิงค์เทรดหลักก็มีประโยชน์อย่างมากในบางสถานการณ์ที่เฉพาะเจาะจง
- การทำงานร่วมกันของ View ที่ซับซ้อน: เมื่อทดสอบ UI แบบไฮบริดที่มีทั้ง Compose และ Android View เดิม การจัดการ View มักจะต้องทำงานใน เทรดหลัก ตอนนี้คุณโต้ตอบกับ View และยืนยันในโหนด Compose ได้ ตามลำดับโดยไม่ต้องสลับบริบทของเธรดอยู่ตลอดเวลา
- การเปลี่ยนแปลงสถานะแบบซิงโครนัส: หากสถาปัตยกรรมของคุณอาศัยตัวยึดสถานะที่ผูกกับเทรดหลักอย่างเคร่งครัด ตอนนี้คุณสามารถเปลี่ยนสถานะและรอให้ UI ของ 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 hierarchy จะดำเนินการในบล็อกเดียวกันได้
@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: หน้า Landing Page หลักของการทดสอบ Android จะให้มุมมองที่กว้างขึ้นเกี่ยวกับพื้นฐานและเทคนิคการทดสอบ
- หลักพื้นฐานของการทดสอบ: ดูข้อมูลเพิ่มเติม เกี่ยวกับแนวคิดหลักเบื้องหลังการทดสอบแอป Android
- การทดสอบในเครื่อง: คุณสามารถเรียกใช้การทดสอบบางอย่าง ในเครื่องบนเวิร์กสเตชันของคุณเองได้
- การทดสอบที่วัดคุม: คุณควรเรียกใช้การทดสอบที่วัดคุมด้วย กล่าวคือ การทดสอบที่ทำงานโดยตรงในอุปกรณ์
- การรวมอย่างต่อเนื่อง: การรวมอย่างต่อเนื่องช่วยให้คุณผสานรวมการทดสอบเข้ากับไปป์ไลน์การติดตั้งใช้งานได้
- ทดสอบขนาดหน้าจอต่างๆ: เนื่องจากผู้ใช้มีอุปกรณ์ให้เลือกมากมาย คุณจึงควรทดสอบขนาดหน้าจอต่างๆ
- Espresso: แม้ว่า Espresso จะออกแบบมาสำหรับ UI ที่อิงตาม View แต่ความรู้เกี่ยวกับ Espresso ก็ยังเป็นประโยชน์สำหรับบางแง่มุมของการทดสอบ Compose