โค้ดการทดสอบ 1 หน่วยที่ใช้Coroutine ต้องใช้การดำเนินการเพิ่มเติม เนื่องจากการดำเนินการอาจไม่พร้อมกันและเกิดขึ้นกับชุดข้อความหลายรายการ คู่มือนี้จะกล่าวถึงวิธีทดสอบฟังก์ชันการระงับ โครงสร้างการทดสอบที่คุณต้องคุ้นเคย และวิธีทำให้โค้ดที่ใช้โกโรทีนสามารถทดสอบได้
API ที่ใช้ในคู่มือนี้เป็นส่วนหนึ่งของไลบรารี kotlinx.coroutines.test อย่าลืมเพิ่มอาร์ติแฟกต์เป็นทรัพยากร Dependency ทดสอบในโปรเจ็กต์ของคุณเพื่อให้มีสิทธิ์เข้าถึง API เหล่านี้
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
การเรียกใช้ฟังก์ชันระงับในการทดสอบ
หากต้องการเรียกใช้ฟังก์ชันการระงับในการทดสอบ คุณต้องอยู่ในโครูทีน เนื่องจากฟังก์ชันการทดสอบ JUnit ไม่ได้ระงับฟังก์ชันต่างๆ คุณจึงต้องเรียกใช้เครื่องมือสร้าง Coroutine ภายในการทดสอบเพื่อเริ่ม Coroutine ใหม่
runTest
คือเครื่องมือสร้างโอโรทีนที่ออกแบบมาเพื่อการทดสอบ ใช้ค่านี้เพื่อห่อหุ้มการทดสอบที่มีโครูทีน โปรดทราบว่าโครูทีนออกฤทธิ์ได้ไม่เพียงแค่ในหน่วยงานทดสอบเท่านั้น แต่ยังเริ่มจากวัตถุที่ใช้ในการทดสอบด้วย
suspend fun fetchData(): String { delay(1000L) return "Hello world" } @Test fun dataShouldBeHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) }
โดยทั่วไป คุณควรมีการเรียก runTest
1 คำขอต่อการทดสอบ และขอแนะนำให้ใช้เนื้อความของนิพจน์
การรวมโค้ดการทดสอบใน runTest
จะทำหน้าที่ทดสอบฟังก์ชันการระงับพื้นฐาน และจะข้ามการหน่วงเวลาของโครูทีนโดยอัตโนมัติ ทำให้การทดสอบข้างต้นเสร็จสมบูรณ์เร็วกว่า 1 วินาที
อย่างไรก็ตาม ยังมีข้อควรพิจารณาเพิ่มเติม ทั้งนี้ขึ้นอยู่กับสิ่งที่เกิดขึ้นในโค้ดของคุณที่อยู่ในการทดสอบ:
- เมื่อโค้ดสร้างโครูทีนใหม่นอกเหนือจากโครูทีนทดสอบระดับบนสุดที่
runTest
สร้างขึ้น คุณจะต้องควบคุมวิธีกำหนดเวลาโครูทีนใหม่เหล่านั้นโดยเลือกTestDispatcher
ที่เหมาะสม - หากโค้ดของคุณย้ายการดำเนินการ Coroutine ไปยังผู้มอบหมายงานคนอื่นๆ (เช่น โดยใช้
withContext
) โดยทั่วไปrunTest
จะยังคงใช้งานได้ แต่จะไม่มีการข้ามการหน่วงเวลาอีกต่อไป และการทดสอบจะคาดการณ์ได้น้อยลงเนื่องจากโค้ดทำงานในหลายเทรด ด้วยเหตุผลเหล่านี้ ในการทดสอบ คุณจึงควรแทรกผู้ติดต่อในการทดสอบเพื่อแทนที่เจ้าหน้าที่จ่ายงานจริง
TestDispatchers
TestDispatchers
คือการติดตั้งใช้งาน CoroutineDispatcher
เพื่อวัตถุประสงค์ในการทดสอบ คุณจะต้องใช้ TestDispatchers
หากมีการสร้างโครูทีนใหม่ในระหว่างการทดสอบ เพื่อให้คาดการณ์โครูทีนใหม่ได้
มีการใช้งาน TestDispatcher
อยู่ 2 แบบ ได้แก่ StandardTestDispatcher
และ UnconfinedTestDispatcher
ซึ่งกำหนดเวลาต่างๆ สำหรับโครูทีนที่เพิ่งเริ่มใหม่ ทั้งคู่ใช้ TestCoroutineScheduler
เพื่อควบคุมเวลาเสมือนและจัดการโครูทีนในการวิ่งภายในการทดสอบ
ควรมีอินสแตนซ์เครื่องจัดตารางเวลาเพียง 1 รายการในการทดสอบ โดยแชร์ระหว่าง TestDispatchers
ทั้งหมด โปรดดูการแทรก TestDispatchers เพื่อเรียนรู้เกี่ยวกับการแชร์เครื่องจัดตารางเวลา
ในการเริ่มต้นโครูทีนทดสอบระดับบนสุด runTest
จะสร้าง TestScope
ซึ่งเป็นการติดตั้งใช้งาน CoroutineScope
ซึ่งจะใช้ TestDispatcher
เสมอ หากไม่ได้ระบุไว้ TestScope
จะสร้าง StandardTestDispatcher
โดยค่าเริ่มต้น และใช้เพื่อเรียกใช้ Coroutine ทดสอบระดับบนสุด
runTest
จะติดตาม coroutine ที่ต่อคิวอยู่ในเครื่องจัดตารางเวลาที่ผู้แจกจ่าย TestScope
ใช้ และจะไม่ส่งคืนอีกตราบใดที่ยังมีงานที่รอดำเนินการอยู่ในเครื่องจัดตารางเวลานั้น
ผู้สั่งทดสอบมาตรฐาน
เมื่อคุณเริ่มโครูทีนใหม่ใน StandardTestDispatcher
โครูทีนจะจัดคิวอยู่ในเครื่องจัดตารางเวลาที่สำคัญเพื่อให้เรียกใช้เมื่อใดก็ตามที่เทรดทดสอบใช้งานได้ฟรี หากต้องการให้โครูทีนใหม่เหล่านี้ทํางาน คุณต้องตอบกลับชุดข้อความทดสอบ (เพิ่มพื้นที่ว่างเพื่อให้โครูทีนอื่นๆ ใช้ได้) ลักษณะการจัดคิวนี้ช่วยให้คุณควบคุมโครูทีนใหม่ในระหว่างการทดสอบได้อย่างแม่นยำ ทั้งยังคล้ายกับการจัดตารางเวลาของโครูทีนในโค้ดการผลิต
หากเทรดทดสอบไม่แสดงระหว่างการดำเนินการของ coroutine ทดสอบระดับบนสุด โครูทีนใหม่จะทำงานหลังจากที่ทดสอบโครูทีนเสร็จแล้วเท่านั้น (แต่ก่อนแสดงผล runTest
)
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
การให้โกรูทีนทดสอบเพื่อให้โครูทีนที่อยู่ในคิวทำงานมีหลายวิธีด้วยกัน การเรียกเหล่านี้ทั้งหมดจะทำให้โครูทีนอื่นๆ ทำงานในเทรดทดสอบก่อนแสดงผลได้
advanceUntilIdle
: เรียกใช้โครูทีนอื่นๆ ทั้งหมดในเครื่องจัดตารางเวลาจนกว่าจะไม่มีสิ่งใดเหลืออยู่ในคิว ตัวเลือกนี้เป็นตัวเลือกเริ่มต้นที่ดีในการให้โครูทีนที่รอดำเนินการทั้งหมดทำงาน และจะใช้งานได้ในสถานการณ์การทดสอบส่วนใหญ่advanceTimeBy
: เลื่อนเวลาเสมือนให้ครบตามจำนวนที่กำหนด และเรียกใช้โครูทีนที่กำหนดให้ทำงานก่อนเวลาดังกล่าวในเวลาเสมือนrunCurrent
: เรียกใช้โครูทีนที่กำหนดเวลาไว้ตามเวลาเสมือนปัจจุบัน
ในการแก้ไขการทดสอบก่อนหน้า คุณใช้ advanceUntilIdle
เพื่ออนุญาตให้โครูทีนที่รอดำเนินการ 2 รายการทำงานได้ก่อนที่จะยืนยันต่อ
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } advanceUntilIdle() // Yields to perform the registrations assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
ผู้มอบหมายการทดสอบที่ไม่จํากัด
เมื่อเริ่มโศกนาฏกรรมใหม่ใน UnconfinedTestDispatcher
จะเริ่มด้วยใจจดใจจ่อในชุดข้อความปัจจุบัน ซึ่งหมายความว่าจะเริ่มทำงานทันทีโดยไม่ต้องรอให้เครื่องมือสร้าง Coroutine กลับมา ในหลายกรณี ลักษณะการส่งแบบนี้จะส่งผลให้โค้ดทดสอบง่ายขึ้น เนื่องจากคุณไม่จำเป็นต้องแสดงเธรดทดสอบด้วยตนเองเพื่อให้โครูทีนใหม่ทำงาน
อย่างไรก็ตาม ลักษณะการทำงานนี้จะแตกต่างจากสิ่งที่คุณจะเห็นในการใช้งานจริงกับผู้มอบหมายงานที่ไม่ใช่ผู้ทดสอบ หากการทดสอบมุ่งเน้นที่การเกิดขึ้นพร้อมกัน ให้ใช้ StandardTestDispatcher
แทน
หากต้องการใช้ผู้มอบหมายงานนี้สำหรับโครูทีนทดสอบระดับบนสุดใน runTest
แทนอินสแตนซ์เริ่มต้น ให้สร้างอินสแตนซ์และส่งต่ออินสแตนซ์เป็นพารามิเตอร์ การทำเช่นนี้จะทำให้โครูทีนใหม่ที่สร้างขึ้นภายใน runTest
ทำงานอย่างตั้งใจ เนื่องจากจะรับค่าเครื่องมือจัดการจาก TestScope
@Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
ในตัวอย่างนี้ การเรียกใช้การเปิดตัวจะเริ่มต้นโครูทีนใหม่อย่างตั้งใจใน UnconfinedTestDispatcher
ซึ่งหมายความว่าการเรียกใช้แต่ละครั้งเพื่อเริ่มใช้งานจะกลับมาก็ต่อเมื่อการลงทะเบียนเสร็จสมบูรณ์แล้วเท่านั้น
อย่าลืมว่า UnconfinedTestDispatcher
จะเริ่มต้นโครูทีนใหม่อย่างตั้งใจ แต่ไม่ได้หมายความว่าจะเรียกใช้โครูทีนได้สำเร็จอย่างตั้งใจเช่นกัน หากโครูทีนใหม่พักการทำงาน โครูทีนอื่นๆ จะกลับมาทำงานอีกครั้ง
ตัวอย่างเช่น Coroutine ที่เปิดตัวใหม่ภายในการทดสอบนี้จะลงทะเบียน Alice แต่จะระงับเมื่อมีการเรียกใช้ delay
วิธีนี้จะช่วยให้โครูทีนระดับบนสุดดำเนินการยืนยันได้ และการทดสอบจะไม่สำเร็จเนื่องจากบ็อบยังไม่ได้ลงทะเบียน
@Test fun yieldingTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") delay(10L) userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
การแทรกเจ้าหน้าที่ทดสอบ
โค้ดที่อยู่ระหว่างทดสอบอาจใช้ผู้มอบหมายงานเพื่อสลับชุดข้อความ (โดยใช้ withContext
) หรือเริ่มโครูทีนใหม่ เมื่อมีการเรียกใช้โค้ดกับชุดข้อความหลายรายการพร้อมกัน การทดสอบอาจไม่น่าเชื่อถือ การยืนยันสิทธิ์ในเวลาที่เหมาะสมหรือรอให้งานเสร็จสมบูรณ์อาจเป็นเรื่องยากหากกำลังทำงานในเทรดเบื้องหลังซึ่งคุณควบคุมไม่ได้
ในการทดสอบ ให้แทนที่ผู้มอบหมายงานเหล่านี้ด้วยอินสแตนซ์ของ TestDispatchers
ซึ่งมีประโยชน์หลายประการดังนี้
- โค้ดจะทำงานในเทรดทดสอบเดี่ยว ทำให้การทดสอบเป็นเชิงกำหนดได้มากขึ้น
- คุณสามารถควบคุมวิธีกำหนดเวลาและดำเนินการของโครูทีนใหม่
- TestDispatchers จะใช้เครื่องจัดตารางเวลาสำหรับเวลาเสมือน ซึ่งจะข้ามการหน่วงเวลาโดยอัตโนมัติและช่วยให้คุณเลื่อนเวลาได้ด้วยตนเอง
ใช้การแทรก Dependency เพื่อระบุ
ผู้แจกจ่ายงานในชั้นเรียน ทำให้สามารถแทนที่ผู้แจกจ่ายงานจริง
การทดสอบ ในตัวอย่างเหล่านี้ เราจะแทรก CoroutineDispatcher
แต่คุณยังสามารถ
แทรก
CoroutineContext
ซึ่งทำให้การทดสอบมีความยืดหยุ่นมากขึ้น
สำหรับคลาสที่ริเริ่มโครูทีน คุณสามารถแทรก CoroutineScope
ได้ด้วย
แทนผู้มอบหมายงาน ตามที่ระบุไว้ในการแทรกขอบเขต
TestDispatchers
จะสร้างเครื่องจัดตารางเวลาใหม่โดยค่าเริ่มต้นเมื่อมีการสร้างอินสแตนซ์ ใน runTest
คุณสามารถเข้าถึงพร็อพเพอร์ตี้ testScheduler
ของ TestScope
และส่งต่อไปยัง TestDispatchers
ที่สร้างขึ้นใหม่ ซึ่งจะแชร์ความเข้าใจเกี่ยวกับเวลาเสมือน และวิธีการอย่าง advanceUntilIdle
จะเรียกใช้โครูทีนกับผู้มอบหมายงานทุกคนเพื่อทำการทดสอบ
ในตัวอย่างต่อไปนี้ คุณสามารถดูคลาส Repository
ที่สร้างโครูทีนใหม่โดยใช้ผู้มอบหมายงาน IO
ในเมธอด initialize
และเปลี่ยนผู้โทรเป็นผู้มอบหมายงาน IO
ในเมธอด fetchData
// Example class demonstrating dispatcher use cases class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) val initialized = AtomicBoolean(false) // A function that starts a new coroutine on the IO dispatcher fun initialize() { scope.launch { initialized.set(true) } } // A suspending function that switches to the IO dispatcher suspend fun fetchData(): String = withContext(ioDispatcher) { require(initialized.get()) { "Repository should be initialized first" } delay(500L) "Hello world" } }
ในการทดสอบ คุณสามารถแทรกการใช้งาน TestDispatcher
เพื่อแทนที่ผู้มอบหมายงาน IO
ในตัวอย่างด้านล่าง เราแทรก StandardTestDispatcher
ลงในที่เก็บ และใช้ advanceUntilIdle
เพื่อตรวจสอบว่าโครูทีนใหม่เริ่มทำงานใน initialize
เสร็จสิ้นแล้วก่อนดำเนินการต่อ
fetchData
จะได้ประโยชน์จากการทำงานใน TestDispatcher
ด้วย เนื่องจากจะทำงานในเทรดทดสอบและข้ามการหน่วงเวลาที่มีระหว่างการทดสอบ
class RepositoryTest { @Test fun repoInitWorksAndDataIsHelloWorld() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = Repository(dispatcher) repository.initialize() advanceUntilIdle() // Runs the new coroutine assertEquals(true, repository.initialized.get()) val data = repository.fetchData() // No thread switch, delay is skipped assertEquals("Hello world", data) } }
โครูทีนใหม่ที่เริ่มต้นใน TestDispatcher
สามารถทำขั้นสูงได้ด้วยตนเองดังที่แสดงด้านบนด้วย initialize
อย่างไรก็ตาม โปรดทราบว่าวิธีนี้เป็นไปไม่ได้หรือไม่เป็นที่ต้องการในโค้ดการผลิต อย่างไรก็ตาม วิธีนี้ควรได้รับการออกแบบใหม่ให้เป็นการระงับ (สำหรับการดำเนินการตามลำดับ) หรือแสดงผลค่า Deferred
(สำหรับการดำเนินการพร้อมกัน) แทน
เช่น คุณใช้ async
เพื่อเริ่มโครูทีนใหม่และสร้าง Deferred
ได้โดยทำดังนี้
class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) fun initialize() = scope.async { // ... } }
วิธีนี้ช่วยให้คุณawait
โค้ดนี้เสร็จสมบูรณ์ได้อย่างปลอดภัยทั้งในการทดสอบและโค้ดเวอร์ชันที่ใช้งานจริง
@Test fun repoInitWorks() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = BetterRepository(dispatcher) repository.initialize().await() // Suspends until the new coroutine is done assertEquals(true, repository.initialized.get()) // ... }
runTest
จะรอให้โครูทีนที่รอดำเนินการดำเนินการเสร็จสมบูรณ์ก่อนกลับมาหากโครูทีนอยู่ใน TestDispatcher
ที่แชร์เครื่องจัดตารางเวลาด้วย นอกจากนี้ ยังจะรอ coroutine ที่เป็นลูกของ coroutine ทดสอบระดับบนสุดด้วย ถึงแม้ว่าจะอยู่บนแอพพลิเมนต์อื่นๆ ก็ตาม (จนถึงระยะหมดเวลาที่ระบุไว้โดยพารามิเตอร์ dispatchTimeoutMs
ซึ่งคือ 60 วินาทีโดยค่าเริ่มต้น)
การตั้งค่าผู้มอบหมายงานหลัก
ในการทดสอบหน่วยในเครื่อง ผู้มอบหมายงาน Main
ที่รวมชุดข้อความ UI ของ Android ไว้จะไม่สามารถใช้ได้ เนื่องจากการทดสอบเหล่านี้จะดำเนินการใน JVM ในเครื่อง ไม่ใช่อุปกรณ์ Android หากโค้ดที่อยู่ในการทดสอบอ้างอิงเทรดหลัก จะมีข้อยกเว้นในระหว่างการทดสอบ 1 หน่วย
ในบางกรณี คุณสามารถแทรกผู้มอบหมายงาน Main
ได้ด้วยวิธีเดียวกับผู้มอบหมายงานคนอื่นๆ ดังที่อธิบายไว้ในส่วนก่อนหน้า โดยคุณสามารถแทนที่ด้วย TestDispatcher
ในการทดสอบได้ อย่างไรก็ตาม API บางรายการ เช่น viewModelScope
จะใช้เครื่องมือจัดการ Main
แบบฮาร์ดโค้ดในขั้นสูง
ตัวอย่างการติดตั้งใช้งาน ViewModel
ที่ใช้ viewModelScope
ในการเปิดใช้ Coroutine ที่โหลดข้อมูลมีดังนี้
class HomeViewModel : ViewModel() { private val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
หากต้องการแทนที่ผู้มอบหมายงาน Main
ด้วย TestDispatcher
ในทุกกรณี ให้ใช้ฟังก์ชัน Dispatchers.setMain
และ Dispatchers.resetMain
class HomeViewModelTest { @Test fun settingMainDispatcher() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } } }
หากผู้มอบหมายงานของ Main
ถูกแทนที่ด้วย TestDispatcher
TestDispatchers
ที่สร้างขึ้นใหม่จะใช้เครื่องจัดตารางเวลาจากผู้มอบหมายงานของ Main
โดยอัตโนมัติ รวมถึง StandardTestDispatcher
ที่ runTest
สร้างขึ้นหากไม่มีการส่งไปยังผู้มอบหมายงานรายอื่น
ซึ่งช่วยให้มั่นใจได้ว่ามีเพียงเครื่องจัดตารางเวลาเพียงเครื่องเดียวที่ใช้ในระหว่างการทดสอบ เพื่อให้วิธีนี้ทำงานได้ โปรดสร้างอินสแตนซ์ TestDispatcher
อื่นๆ ทั้งหมดหลังจากการเรียก Dispatchers.setMain
รูปแบบทั่วไปที่จะหลีกเลี่ยงการทำซ้ำโค้ดที่แทนที่เครื่องจัดการ Main
ในการทดสอบแต่ละครั้งคือการดึงข้อมูลลงในกฎการทดสอบของ JUnit ดังนี้
// Reusable JUnit4 TestRule to override the Main dispatcher class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } } class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun settingMainDispatcher() = runTest { // Uses Main’s scheduler val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
การใช้กฎนี้จะใช้ UnconfinedTestDispatcher
โดยค่าเริ่มต้น แต่สามารถส่ง StandardTestDispatcher
เป็นพารามิเตอร์ได้หากผู้มอบหมายงาน Main
ไม่ควรทำงานอย่างตั้งใจในคลาสการทดสอบที่กำหนด
เมื่อต้องการใช้อินสแตนซ์ TestDispatcher
ในเนื้อหาทดสอบ คุณจะใช้ testDispatcher
จากกฎซ้ำได้ตราบใดที่เป็นประเภทที่ต้องการ หากคุณต้องการระบุประเภทของ TestDispatcher
ที่ใช้ในการทดสอบให้ชัดเจน หรือหากต้องการ TestDispatcher
ที่ไม่ใช่ประเภทที่ใช้กับ Main
คุณสร้าง TestDispatcher
ใหม่ได้ภายใน runTest
เนื่องจากผู้มอบหมายงานของ Main
ได้รับการตั้งค่าเป็น TestDispatcher
TestDispatchers
ที่สร้างขึ้นใหม่จะแชร์เครื่องจัดตารางเวลาโดยอัตโนมัติ
class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler // Use the UnconfinedTestDispatcher from the Main dispatcher val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) // Create a new StandardTestDispatcher (uses Main’s scheduler) val standardRepo = Repository(StandardTestDispatcher()) } }
การสร้างผู้มอบหมายงานนอกการทดสอบ
ในบางกรณี คุณอาจต้องใช้ TestDispatcher
นอกวิธีการทดสอบ เช่น ระหว่างการเริ่มต้นใช้งานพร็อพเพอร์ตี้ในคลาสการทดสอบ
class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* What TestDispatcher? */) @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun someRepositoryTest() = runTest { // Test the repository... // ... } }
หากคุณแทนที่ผู้มอบหมายงาน Main
ตามที่แสดงในส่วนก่อนหน้านี้ TestDispatchers
ที่สร้างขึ้นหลังจากมีการแทนที่ผู้มอบหมายงานของ Main
แล้วจะแชร์เครื่องจัดตารางเวลาของตนโดยอัตโนมัติ
แต่ไม่ใช่กรณีเช่นนี้ สำหรับ TestDispatchers
ที่สร้างขึ้นเป็นพร็อพเพอร์ตี้ของคลาสการทดสอบ หรือ TestDispatchers
ที่สร้างขึ้นระหว่างการเริ่มต้นพร็อพเพอร์ตี้ในคลาสการทดสอบ ระบบจะเริ่มการทำงานเหล่านี้ก่อนที่จะมีการแทนที่ผู้มอบหมายงาน Main
ดังนั้นจึงจะสร้างเครื่องจัดตารางเวลาใหม่
โปรดสร้างพร็อพเพอร์ตี้ MainDispatcherRule
ก่อนเพื่อให้แน่ใจว่ามีเครื่องจัดตารางเวลาเพียง 1 รายการในการทดสอบ จากนั้นนำผู้มอบหมายงานกลับมาใช้ใหม่ (หรือเครื่องจัดตารางเวลา ถ้าคุณต้องการ TestDispatcher
ประเภทอื่น) ในการเริ่มต้นของพร็อพเพอร์ตี้ระดับชั้นเรียนอื่นๆ ตามความจำเป็น
class RepositoryTestWithRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() private val repository = ExampleRepository(mainDispatcherRule.testDispatcher) @Test fun someRepositoryTest() = runTest { // Takes scheduler from Main // Any TestDispatcher created here also takes the scheduler from Main val newTestDispatcher = StandardTestDispatcher() // Test the repository... } }
โปรดทราบว่าทั้ง runTest
และ TestDispatchers
ที่สร้างขึ้นภายในการทดสอบจะยังคงแชร์เครื่องจัดตารางเวลาของผู้มอบหมายงาน Main
โดยอัตโนมัติ
หากคุณไม่ได้แทนที่ผู้มอบหมายงาน Main
ให้สร้าง TestDispatcher
แรกของคุณ (ซึ่งจะสร้างเครื่องจัดตารางเวลาใหม่) เป็นพร็อพเพอร์ตี้ของชั้นเรียน จากนั้นส่งเครื่องจัดตารางเวลาดังกล่าวไปยังการเรียกใช้ runTest
แต่ละรายการและ TestDispatcher
ใหม่ที่สร้างขึ้นแต่ละรายการด้วยตนเอง ทั้งที่เป็นพร็อพเพอร์ตี้และภายในการทดสอบ
class RepositoryTest { // Creates the single test scheduler private val testDispatcher = UnconfinedTestDispatcher() private val repository = ExampleRepository(testDispatcher) @Test fun someRepositoryTest() = runTest(testDispatcher.scheduler) { // Take the scheduler from the TestScope val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler) // Or take the scheduler from the first dispatcher, they’re the same val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler) // Test the repository... } }
ในตัวอย่างนี้ ระบบจะส่งเครื่องจัดตารางเวลาจากผู้มอบหมายงานคนแรกไปยัง runTest
การดำเนินการนี้จะสร้าง StandardTestDispatcher
ใหม่สำหรับ TestScope
โดยใช้เครื่องจัดตารางเวลาดังกล่าว นอกจากนี้คุณยังสามารถส่งต่อให้กับผู้มอบหมายไปยัง runTest
โดยตรงเพื่อใช้โครูทีนทดสอบกับผู้มอบหมายงานคนดังกล่าว
การสร้าง TestScope ของคุณเอง
คุณอาจต้องเข้าถึง TestScope
นอกหน่วยทดสอบ เช่นเดียวกับ TestDispatchers
แม้ว่า runTest
จะสร้าง TestScope
ขั้นสูงโดยอัตโนมัติ แต่คุณก็สร้าง TestScope
ของคุณเองเพื่อใช้กับ runTest
ได้เช่นกัน
เมื่อดำเนินการนี้ โปรดโทรหา runTest
ใน TestScope
ที่คุณสร้างไว้
class SimpleExampleTest { val testScope = TestScope() // Creates a StandardTestDispatcher @Test fun someTest() = testScope.runTest { // ... } }
โค้ดข้างต้นจะสร้าง StandardTestDispatcher
สำหรับ TestScope
โดยปริยาย รวมทั้งเครื่องจัดตารางเวลาใหม่ คุณยังสามารถสร้างออบเจ็กต์เหล่านี้อย่างชัดแจ้งได้อีกด้วย ซึ่งจะเป็นประโยชน์ในกรณีที่คุณต้องผสานรวมกับการตั้งค่าการแทรกทรัพยากร Dependency
class ExampleTest { val testScheduler = TestCoroutineScheduler() val testDispatcher = StandardTestDispatcher(testScheduler) val testScope = TestScope(testDispatcher) @Test fun someTest() = testScope.runTest { // ... } }
การแทรกขอบเขต
ในกรณีที่มีชั้นเรียนที่สร้างโครูทีนซึ่งคุณต้องควบคุม
คุณสามารถแทรกขอบเขต Coroutine ลงในคลาสนั้น
แล้วแทนที่ด้วย
TestScope
ในการทดสอบ
ในตัวอย่างต่อไปนี้ คลาส UserState
จะขึ้นอยู่กับ UserRepository
เพื่อลงทะเบียนผู้ใช้ใหม่และดึงข้อมูลรายชื่อผู้ใช้ที่ลงทะเบียน เป็นการโทรเหล่านี้
ไปยัง UserRepository
กำลังระงับการเรียกฟังก์ชัน UserState
จะใช้คำสั่งที่แทรก
CoroutineScope
เพื่อเริ่มโคโรทีนใหม่ภายในฟังก์ชัน registerUser
class UserState( private val userRepository: UserRepository, private val scope: CoroutineScope, ) { private val _users = MutableStateFlow(emptyList<String>()) val users: StateFlow<List<String>> = _users.asStateFlow() fun registerUser(name: String) { scope.launch { userRepository.register(name) _users.update { userRepository.getAllUsers() } } } }
หากต้องการทดสอบชั้นเรียนนี้ คุณสามารถสอบให้ผ่านใน TestScope
จาก runTest
เมื่อสร้าง
ออบเจ็กต์ UserState
ดังนี้
class UserStateTest { @Test fun addUserTest() = runTest { // this: TestScope val repository = FakeUserRepository() val userState = UserState(repository, scope = this) userState.registerUser("Mona") advanceUntilIdle() // Let the coroutine complete and changes propagate assertEquals(listOf("Mona"), userState.users.value) } }
หากต้องการแทรกขอบเขตนอกฟังก์ชันทดสอบ เช่น ลงในออบเจ็กต์ใต้ ที่สร้างขึ้นเป็นพร็อพเพอร์ตี้ในคลาสการทดสอบ โปรดดู การสร้าง TestScope ของคุณเอง