วิธีทดสอบยูนิตหรือโมดูลที่สื่อสารกับ flow จะขึ้นอยู่กับว่ารายการทดสอบใช้ flow เป็นอินพุตหรือเอาต์พุต
- หากเรื่องที่เราทดสอบสังเกตเห็นการไหลเวียน คุณสามารถสร้างการไหลเวียนภายในการอ้างอิงจำลองที่คุณควบคุมจากการทดสอบได้
- หากหน่วยหรือข้อบังคับแสดงโฟลว์ คุณจะอ่านและยืนยันรายการอย่างน้อย 1 รายการที่โฟลว์แสดงในการทดสอบได้
การสร้างโปรดิวเซอร์ปลอม
เมื่อสิ่งที่ทดสอบเป็นผู้บริโภคของโฟลว์ วิธีทดสอบที่พบบ่อยวิธีหนึ่งคือการแทนที่ผู้ผลิตด้วยการติดตั้งใช้งานจำลอง ตัวอย่างเช่น เมื่อมีคลาสที่สังเกตการณ์ที่เก็บข้อมูลที่ดึงข้อมูลจากแหล่งข้อมูล 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()
ฟังก์ชันนี้จะรอจนกว่าจะได้รับการส่งรายการแรก จากนั้นจึงส่งสัญญาณการยกเลิกไปยังโปรดิวเซอร์
@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 myRepositoryTest() = runTest {
// 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
}
เมื่อใช้การจําลองนี้ในการทดสอบ คุณสามารถสร้าง coroutine การเก็บรวบรวมที่จะรับค่าจาก Repository
อย่างต่อเนื่อง ในตัวอย่างนี้ เราจะรวบรวมรายการเหล่านั้นไว้ในรายการ แล้วทำการยืนยันเนื้อหา
@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
การเรียกเก็บเงินที่รวบรวมข้อมูลจึงไม่มีวันแสดงผล การเริ่ม Coroutine ที่รวบรวมใน TestScope.backgroundScope
จะทำให้ Coroutine ถูกยกเลิกก่อนการทดสอบสิ้นสุด ไม่เช่นนั้น runTest
จะรอให้เสร็จสมบูรณ์อยู่เรื่อยๆ ซึ่งทําให้การทดสอบหยุดตอบกลับและในที่สุดก็ทํางานไม่สําเร็จ
โปรดสังเกตวิธีใช้ UnconfinedTestDispatcher
กับ Coroutine การเก็บรวบรวมที่นี่ วิธีนี้ช่วยให้มั่นใจว่าระบบจะเปิดใช้ coroutine การเก็บรวบรวมทันทีและพร้อมรับค่าหลังจากที่ launch
แสดงผล
การใช้ Turbine
ไลบรารี Turbine ของบุคคลที่สามมี API ที่สะดวกสำหรับการสร้าง Coroutine การเก็บรวบรวม รวมถึงฟีเจอร์อื่นๆ ที่สะดวกสำหรับการทดสอบ Flow ดังนี้
@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
มักใช้ในโมเดลมุมมอง
แม้ว่าคุณจะยืนยันใน value
ของ StateFlow
ในการทดสอบ แต่คุณก็จะต้องสร้างเครื่องมือรวบรวมข้อมูล ซึ่งอาจเป็นตัวรวบรวมที่ว่างเปล่า
@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 ใน Android
- โฟลว์ Kotlin ใน Android
StateFlow
และSharedFlow
- แหล่งข้อมูลเพิ่มเติมสำหรับโคโริวทีนและโฟลว์ของ Kotlin