ซิงค์ข้อมูลการทดสอบของคุณ

การทดสอบ 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