วิธีที่คุณทดสอบหน่วยหรือโมดูลที่สื่อสารกับ flow ขึ้นอยู่กับว่าวัตถุที่กำลังทดสอบใช้โฟลว์เป็นอินพุตหรือเอาต์พุต
- หากผู้เข้าร่วมทดสอบสังเกตเห็นโฟลว์ คุณจะสร้างโฟลว์ภายใน ทรัพยากร Dependency ปลอมที่คุณควบคุมได้จากการทดสอบ
- หากหน่วยหรือโมดูลแสดงโฟลว์ คุณจะอ่านและยืนยันได้ หรือ หลายรายการที่ปล่อยออกมาจากขั้นตอนหนึ่งในการทดสอบ
การสร้างโปรดิวเซอร์ปลอม
เมื่อผู้เข้าร่วมทดสอบเป็นผู้บริโภคของขั้นตอนใดๆ วิธีทั่วไปในการทดสอบ คือการแทนที่ผู้ผลิตด้วยการติดตั้งใช้งานปลอม ตัวอย่างเช่น ระบุ คลาสที่สังเกตที่เก็บซึ่งนำข้อมูลจากแหล่งข้อมูล 2 แหล่งมาไว้ใน เวอร์ชันที่ใช้งานจริง:
หากต้องการทดสอบเชิงกำหนด คุณอาจแทนที่ที่เก็บและที่เก็บดังกล่าว ทรัพยากร Dependency ที่มีที่เก็บปลอมซึ่งจะส่งข้อมูลปลอมแบบเดียวกันเสมอ
หากต้องการปล่อยชุดค่าที่กำหนดไว้ล่วงหน้าในโฟลว์ ให้ใช้เครื่องมือสร้าง 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
}
เมื่อใช้ข้อมูลปลอมนี้ในการทดสอบ คุณสามารถสร้างโครูทีนสะสมที่จะ
ได้รับค่าจาก 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
ที่เก็บรวบรวมไว้ก็จะไม่กลับมาอีก กำลังเริ่มเก็บโครูทีนใน
TestScope.backgroundScope
ช่วยให้แน่ใจว่าโครูทีนถูกยกเลิกก่อนสิ้นสุดการทดสอบ หรือไม่เช่นนั้น
runTest
จะรออย่างต่อเนื่องจนเสร็จสิ้น ซึ่งทำให้การทดสอบหยุดลง
และล้มเหลวในที่สุด
สังเกตวิธี
UnconfinedTestDispatcher
ใช้สำหรับเก็บค่าโครูทีนที่นี่ วิธีนี้ช่วยให้มั่นใจว่าการรวบรวม
เปิดตัว Coroutine ด้วยความตั้งใจและพร้อมรับค่าหลังจากวันที่ launch
ที่เกินออกมา
การใช้กังหัน
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())
}
}
โปรดดู เอกสารประกอบของlibrary เกี่ยวกับ รายละเอียดเพิ่มเติม
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
}
การทำงานร่วมกับ StateFlows ที่สร้างโดย 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.WhileSubsribed
บ่อย
ใน ViewModels
แม้ว่าคุณจะยืนยันที่ 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