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