Cara Anda menguji unit atau modul yang berkomunikasi dengan flow bergantung pada apakah subjek yang sedang diuji tersebut menggunakan flow sebagai input atau output.
- Jika subjek yang sedang diuji mengamati flow, Anda dapat membuat flow dalam dependensi palsu yang dapat Anda kontrol dari pengujian.
- Jika unit atau modul menampilkan flow, Anda dapat membaca dan memverifikasi satu atau beberapa item yang dimunculkan oleh flow dalam pengujian.
Membuat produser palsu
Jika subjek yang sedang diuji adalah konsumen flow, satu cara umum untuk mengujinya adalah dengan mengganti produser dengan implementasi palsu. Misalnya, dengan mempertimbangkan class yang mengamati repositori yang mengambil data dari dua sumber data dalam produksi:
Untuk membuat pengujian menjadi deterministik, Anda dapat mengganti repositori dan dependensinya dengan repositori palsu yang selalu memunculkan data palsu yang sama:
Untuk memunculkan rangkaian nilai yang telah ditentukan dalam flow, gunakan builder flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
Dalam pengujian, repositori palsu ini dimasukkan untuk menggantikan implementasi yang sebenarnya:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Setelah memiliki kontrol atas output subjek yang sedang diuji, Anda dapat memverifikasi bahwa kontrol tersebut berfungsi sebagaimana mestinya dengan memeriksa outputnya.
Menyatakan kemunculan flow dalam pengujian
Jika subjek yang sedang diuji menampilkan flow, pengujian harus membuat pernyataan tentang elemen aliran data.
Anggaplah repositori contoh sebelumnya menampilkan flow:
Dengan pengujian tertentu, Anda hanya perlu memeriksa kemunculan pertama atau jumlah item terbatas yang berasal dari flow.
Anda dapat menggunakan kemunculan pertama ke flow dengan memanggil first()
. Fungsi
ini akan menunggu hingga item pertama diterima, lalu mengirim
sinyal pembatalan ke produser.
@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)
}
Jika pengujian perlu memeriksa beberapa nilai, pemanggilan toList()
akan menyebabkan
flow menunggu sumber untuk memunculkan semua nilainya, lalu menampilkan nilai tersebut
sebagai daftar. Ini hanya berfungsi untuk aliran data terbatas.
@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)
}
Untuk aliran data yang memerlukan pengumpulan item yang lebih rumit atau
yang tidak menampilkan jumlah item terbatas, Anda dapat menggunakan Flow
API untuk memilih dan mengubah
item. Berikut beberapa contohnya:
// 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)
Pengumpulan berkelanjutan selama pengujian
Mengumpulkan flow menggunakan toList()
seperti yang terlihat dalam contoh sebelumnya menggunakan
collect()
secara internal, dan menangguhkan sampai seluruh daftar hasil siap untuk
ditampilkan.
Untuk menyisipkan tindakan yang menyebabkan flow memunculkan nilai dan pernyataan pada nilai yang ditampilkan, Anda dapat terus mengumpulkan nilai dari flow selama pengujian.
Misalnya, ambil class Repository
berikut untuk diuji, dan
implementasi sumber data palsu yang disertai metode emit
untuk
menghasilkan nilai secara dinamis selama pengujian:
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
}
Saat menggunakan sumber data palsu ini dalam pengujian, Anda dapat membuat coroutine pengumpulan yang akan
terus menerima nilai dari Repository
. Dalam contoh ini, kita
mengumpulkannya ke dalam daftar lalu membuat pernyataan pada kontennya:
@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
}
Karena flow yang ditampilkan oleh Repository
di sini tidak pernah selesai, panggilan toList
yang mengumpulkannya tidak akan pernah ditampilkan. Memulai coroutine pengumpulan di
TestScope.backgroundScope
memastikan bahwa coroutine dibatalkan sebelum akhir pengujian. Jika tidak,
runTest
akan terus menunggu hingga selesai, yang menyebabkan pengujian berhenti
merespons dan pada akhirnya akan gagal.
Perhatikan cara
UnconfinedTestDispatcher
digunakan untuk coroutine pengumpulan di sini. Ini memastikan coroutine pengumpulan
diluncurkan dengan segera dan siap menerima nilai setelah launch
ditampilkan.
Menggunakan Turbine
Turbin pihak ketiga library menawarkan API yang mudah digunakan untuk membuat coroutine pengumpulan sebagai fitur praktis lainnya untuk menguji 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())
}
}
Lihat dokumentasi library untuk detail selengkapnya.
Menguji StateFlow
StateFlow
adalah holder
data yang dapat diamati, yang dapat dikumpulkan untuk mengamati nilai yang disimpannya dari waktu ke waktu sebagai
aliran. Perlu diperhatikan bahwa aliran nilai ini digabungkan. Artinya, jika
nilai disetel dalam StateFlow
dengan cepat, kolektor StateFlow
tersebut tidak
dijamin akan menerima semua nilai antara dan hanya menerima nilai terbaru.
Dalam pengujian, jika ingin mempertahankan penggabungan, Anda dapat mengumpulkan nilai StateFlow
seperti yang dapat dikumpulkan dalam flow lain, termasuk dengan Turbine. Dalam beberapa skenario pengujian,
Anda dapat mencoba mengumpulkan dan menyatakan semua nilai antara.
Namun, sebaiknya perlakukan StateFlow
sebagai holder data dan
buat pernyataan di properti value
-nya. Dengan begitu, pengujian akan memvalidasi
status objek saat ini pada waktu tertentu, dan tidak bergantung pada apakah
terjadi penggabungan atau tidak.
Misalnya, perhatikan ViewModel ini yang mengumpulkan nilai dari Repository
dan
menampilkannya ke UI di 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
}
}
}
}
Implementasi palsu untuk Repository
ini mungkin terlihat seperti berikut:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
Saat menguji ViewModel dengan implementasi palsu ini, Anda dapat menampilkan nilai dari implementasi palsu untuk
memicu update di StateFlow
ViewModel, lalu membuat pernyataan pada
value
yang telah diupdate:
@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
}
Menggunakan StateFlow yang dibuat oleh stateIn
Di bagian sebelumnya, ViewModel menggunakan MutableStateFlow
untuk menyimpan
nilai terbaru yang dimunculkan oleh flow dari Repository
. Ini adalah pola yang umum,
biasanya diimplementasikan dengan cara yang
lebih sederhana dengan menggunakan
stateIn
operator, yang mengonversi aliran dingin menjadi StateFlow
panas:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
Operator stateIn
memiliki parameter SharingStarted
, yang menentukan kapan
operator menjadi aktif dan mulai menggunakan flow yang mendasarinya. Opsi seperti
SharingStarted.Lazily
dan SharingStarted.WhileSubsribed
sering digunakan
di ViewModel.
Meskipun Anda membuat pernyataan pada value
dari StateFlow
dalam pengujian, Anda harus
membuat kolektor. Kolektor dapat berupa kolektor kosong:
@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)
}
Referensi lainnya
- Menguji coroutine Kotlin di Android
- Flow Kotlin di Android
StateFlow
danSharedFlow
- Referensi lainnya untuk coroutine dan flow Kotlin