วิธีทดสอบหน่วยหรือโมดูลที่สื่อสารกับ Flow จะขึ้นอยู่กับว่าออบเจ็กต์ภายใต้การทดสอบใช้ Flow เป็นอินพุตหรือเอาต์พุต
- หาก Subject ที่อยู่ภายใต้การทดสอบสังเกตโฟลว์ คุณจะสร้างโฟลว์ภายใน การอ้างอิงปลอมที่ควบคุมได้จากการทดสอบ
- หากหน่วยหรือโมดูลแสดงโฟลว์ คุณจะอ่านและยืนยันรายการเดียวหรือหลายรายการที่โฟลว์ปล่อยออกมาในการทดสอบได้
การสร้างโปรดิวเซอร์ปลอม
เมื่อเรื่องที่อยู่ภายใต้การทดสอบเป็นผู้ใช้โฟลว์ วิธีทั่วไปในการทดสอบคือการแทนที่ผู้ผลิตด้วยการติดตั้งใช้งานปลอม ตัวอย่างเช่น หากมีคลาสที่สังเกตที่เก็บที่รับข้อมูลจากแหล่งข้อมูล 2 แหล่งในสภาพแวดล้อมการผลิต
หากต้องการทำให้การทดสอบเป็นแบบกำหนด คุณสามารถแทนที่ที่เก็บและ การอ้างอิงด้วยที่เก็บปลอมที่ปล่อยข้อมูลปลอมเดียวกันเสมอได้
หากต้องการส่งค่าที่กำหนดไว้ล่วงหน้าเป็นชุดในโฟลว์ ให้ใช้เครื่องมือสร้าง flow โดยทำดังนี้
class MyFakeRepository : MyRepository { fun observeCount() = flow { emit(ITEM_1) } }
ในการทดสอบ ระบบจะแทรกที่เก็บปลอมนี้เพื่อแทนที่การใช้งานจริง
@Test fun myTest() { // Given a class with fake dependencies: val sut = MyUnitUnderTest(MyFakeRepository()) // Trigger and verify // ... }
ตอนนี้คุณควบคุมเอาต์พุตของออบเจ็กต์ที่อยู่ภายใต้การทดสอบได้แล้ว คุณจึง ยืนยันได้ว่าออบเจ็กต์ทำงานได้อย่างถูกต้องโดยการตรวจสอบเอาต์พุต
การยืนยันการปล่อยก๊าซของโฟลว์ในการทดสอบ
หากเรื่องที่ทดสอบแสดงโฟลว์ การทดสอบต้องทำการยืนยัน ในองค์ประกอบของสตรีมข้อมูล
สมมติว่าที่เก็บของตัวอย่างก่อนหน้าแสดงโฟลว์ดังนี้
ในการทดสอบบางอย่าง คุณจะต้องตรวจสอบเฉพาะการปล่อยครั้งแรกหรือรายการจำนวนจำกัดที่มาจากโฟลว์
คุณสามารถใช้การปล่อยก๊าซครั้งแรกกับโฟลว์ได้โดยโทรหา first() ฟังก์ชันนี้จะรอจนกว่าจะได้รับรายการแรก แล้วจึงส่งสัญญาณการยกเลิกไปยัง Producer
@Test fun myRepositoryTest() = runTest { // Given a repository that combines values from two data sources: val repository = MyRepository(fakeSource1, fakeSource2) // When the repository emits a value val firstItem = repository.counter.first() // Returns the first item in the flow // Then check it's the expected item assertEquals(ITEM_1, firstItem) }
หากการทดสอบต้องตรวจสอบค่าหลายค่า การเรียก toList() จะทำให้โฟลว์
รอให้แหล่งที่มาปล่อยค่าทั้งหมดออกมา แล้วจึงแสดงค่าเหล่านั้นเป็น
รายการ ซึ่งใช้ได้กับสตรีมข้อมูลที่สิ้นสุดแล้วเท่านั้น
@Test fun myRepositoryTestList() = runTest { val repository = MyFakeRepository() // Given a repository with a fake data source that emits ALL_MESSAGES val messages = repository.observeChatMessages().toList() // When all messages are emitted then they should be ALL_MESSAGES assertEquals(ALL_MESSAGES, messages) }
สำหรับสตรีมข้อมูลที่ต้องรวบรวมรายการที่ซับซ้อนมากขึ้นหรือไม่ได้แสดงรายการ
จำนวนจำกัด คุณสามารถใช้ Flow API เพื่อเลือกและเปลี่ยนรูปแบบ
รายการได้ ตัวอย่างเช่น
// Take the second item outputFlow.drop(1).first() // Take the first 5 items outputFlow.take(5).toList() // Takes the first item verifying that the flow is closed after that outputFlow.single() // Finite data streams // Verify that the flow emits exactly N elements (optional predicate) outputFlow.count() outputFlow.count(predicate)
การรวบรวมข้อมูลอย่างต่อเนื่องระหว่างการทดสอบ
การรวบรวมโฟลว์โดยใช้ toList() ตามที่เห็นในตัวอย่างก่อนหน้าจะใช้ collect() ภายใน และจะระงับไว้จนกว่ารายการผลลัพธ์ทั้งหมดจะพร้อมที่จะแสดง
หากต้องการสลับการดำเนินการที่ทำให้โฟลว์ปล่อยค่าและการยืนยันใน ค่าที่ปล่อยออกมา คุณสามารถรวบรวมค่าจากโฟลว์อย่างต่อเนื่องในระหว่าง การทดสอบ
ตัวอย่างเช่น ลองใช้คลาส Repository ต่อไปนี้เพื่อทดสอบ และการติดตั้งใช้งานแหล่งข้อมูลจำลองที่มาพร้อมกันซึ่งมีเมธอด emit เพื่อสร้างค่าแบบไดนามิกระหว่างการทดสอบ
class Repository(private val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } } class FakeDataSource : DataSource { private val flow = MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun counts(): Flow<Int> = flow }
เมื่อใช้การจำลองนี้ในการทดสอบ คุณจะสร้างโครูทีนการรวบรวมที่จะรับค่าจาก Repository อย่างต่อเนื่องได้ ในตัวอย่างนี้ เราจะ
รวบรวมรายการและทำการยืนยันเนื้อหาของรายการ
@OptIn(ExperimentalCoroutinesApi::class) @Test fun continuouslyCollect() = runTest { val dataSource = FakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { repository.scores().toList(values) } dataSource.emit(1) assertEquals(10, values[0]) // Assert on the list contents dataSource.emit(2) dataSource.emit(3) assertEquals(30, values[2]) assertEquals(3, values.size) // Assert the number of items collected }
เนื่องจากโฟลว์ที่แสดงโดย Repository ที่นี่ไม่เคยเสร็จสมบูรณ์ การเรียกใช้ toList
ที่รวบรวมโฟลว์จึงไม่เคยส่งคืน การเริ่มต้นโครูทีนการรวบรวมใน
TestScope.backgroundScope
ช่วยให้มั่นใจได้ว่าโครูทีนจะถูกยกเลิกก่อนสิ้นสุดการทดสอบ ไม่เช่นนั้น runTest จะรอให้การดำเนินการเสร็จสมบูรณ์ต่อไป ซึ่งจะทำให้การทดสอบหยุดตอบสนองและล้มเหลวในที่สุด
โปรดสังเกตว่ามีการใช้
UnconfinedTestDispatcher
สำหรับโครูทีนการรวบรวมที่นี่ ซึ่งจะช่วยให้มั่นใจได้ว่า Coroutine ของการรวบรวมจะเปิดตัวอย่างรวดเร็วและพร้อมรับค่าหลังจาก launch
ส่งคืน
การใช้ Turbine
ไลบรารี Turbine ของบุคคลที่สาม มี API ที่สะดวกสำหรับการสร้างโครูทีนที่รวบรวมข้อมูล รวมถึง ฟีเจอร์อำนวยความสะดวกอื่นๆ สำหรับการทดสอบโฟลว์
@Test fun usingTurbine() = runTest { val dataSource = FakeDataSource() val repository = Repository(dataSource) repository.scores().test { // Make calls that will trigger value changes only within test{} dataSource.emit(1) assertEquals(10, awaitItem()) dataSource.emit(2) awaitItem() // Ignore items if needed, can also use skip(n) dataSource.emit(3) assertEquals(30, awaitItem()) } }
ดูรายละเอียดเพิ่มเติมได้ใน เอกสารประกอบของไลบรารี
การทดสอบ StateFlow
StateFlow เป็นที่เก็บข้อมูลที่สังเกตได้
ซึ่งรวบรวมเพื่อสังเกตค่าที่เก็บไว้เมื่อเวลาผ่านไปในรูปแบบ
สตรีม โปรดทราบว่าสตรีมค่านี้จะรวมกัน ซึ่งหมายความว่าหากตั้งค่าใน StateFlow อย่างรวดเร็ว ระบบจะไม่รับประกันว่าตัวรวบรวมของ StateFlow นั้นจะได้รับค่ากลางทั้งหมด แต่จะได้รับค่าล่าสุดเท่านั้น
ในการทดสอบ หากคำนึงถึงการรวมกัน คุณจะรวบรวมค่าของ StateFlow ได้
เช่นเดียวกับการรวบรวมโฟลว์อื่นๆ รวมถึงโฟลว์ที่ใช้ Turbine การพยายามรวบรวม
และยืนยันค่ากลางทั้งหมดอาจเป็นสิ่งที่ต้องการในสถานการณ์การทดสอบบางอย่าง
อย่างไรก็ตาม โดยทั่วไปเราขอแนะนำให้ถือว่า StateFlow เป็นผู้ถือครองข้อมูลและ
ยืนยันในพร็อพเพอร์ตี้ value แทน วิธีนี้จะช่วยให้การทดสอบตรวจสอบสถานะปัจจุบันของออบเจ็กต์ ณ จุดใดจุดหนึ่ง และไม่ขึ้นอยู่กับว่ามีการรวมกันหรือไม่
ตัวอย่างเช่น ลองใช้ ViewModel นี้ที่รวบรวมค่าจาก Repository และ
แสดงค่าเหล่านั้นใน UI ใน StateFlow
class MyViewModel(private val myRepository: MyRepository) : ViewModel() { private val _score = MutableStateFlow(0) val score: StateFlow<Int> = _score.asStateFlow() fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _score.value = score } } } }
การติดตั้งใช้งานปลอมสำหรับ Repository นี้อาจมีลักษณะดังนี้
class FakeRepository : MyRepository { private val flow = MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun scores(): Flow<Int> = flow }
เมื่อทดสอบ ViewModel ด้วยข้อมูลจำลองนี้ คุณจะส่งค่าจากข้อมูลจำลอง
เพื่อทริกเกอร์การอัปเดตใน StateFlow ของ ViewModel แล้วยืนยันใน value ที่อัปเดตแล้วได้โดยทำดังนี้
@Test fun testHotFakeRepository() = runTest { val fakeRepository = FakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.score.value) // Assert on the initial value // Start collecting values from the Repository viewModel.initialize() // Then we can send in values one by one, which the ViewModel will collect fakeRepository.emit(1) assertEquals(1, viewModel.score.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.score.value) // Assert on the latest value }
การทำงานกับ StateFlow ที่สร้างโดย stateIn
ในส่วนก่อนหน้า ViewModel ใช้ MutableStateFlow เพื่อจัดเก็บ
ค่าล่าสุดที่โฟลว์ปล่อยออกมาจาก Repository นี่เป็นรูปแบบที่พบบ่อย
ซึ่งมักจะใช้ในวิธีที่ง่ายกว่าโดยใช้ตัวดำเนินการ
stateIn
ซึ่งจะเปลี่ยนโฟลว์แบบเย็นเป็นโฟลว์แบบร้อน StateFlow
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() { val score: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) }
ตัวดำเนินการ stateIn มีพารามิเตอร์ SharingStarted ซึ่งกำหนดเวลาที่
จะเปิดใช้งานและเริ่มใช้โฟลว์พื้นฐาน ตัวเลือกต่างๆ เช่น
SharingStarted.Lazily และ SharingStarted.WhileSubscribed มักใช้
ใน ViewModel
แม้ว่าคุณจะยืนยันใน value ของ StateFlow ในการทดสอบ คุณก็ยังต้องสร้างตัวรวบรวม ซึ่งอาจเป็นตัวรวบรวมข้อมูลที่ว่างเปล่าได้ดังนี้
@OptIn(ExperimentalCoroutinesApi::class) @Test fun testLazilySharingViewModel() = runTest { val fakeRepository = HotFakeRepository() val viewModel = MyViewModelWithStateIn(fakeRepository) // Create an empty collector for the StateFlow backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { viewModel.score.collect {} } assertEquals(0, viewModel.score.value) // Can assert initial value // Trigger-assert like before fakeRepository.emit(1) assertEquals(1, viewModel.score.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.score.value) }
แหล่งข้อมูลเพิ่มเติม
- การทดสอบ Kotlin Coroutines ใน Android
- โฟลว์ Kotlin ใน Android
StateFlowและSharedFlow- แหล่งข้อมูลเพิ่มเติมสำหรับโครูทีนและโฟลว์ของ Kotlin