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

การทดสอบ 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 ในบางแง่มุม