Akış ile iletişim kuran birimleri veya modülleri test etme şekliniz, test edilen öznenin akışı giriş veya çıkış olarak kullanıp kullanmadığına bağlıdır.
- Test edilen konu bir akış gözlemliyorsa testlerden kontrol edebileceğiniz sahte bağımlılıklar içinde akışlar oluşturabilirsiniz.
- Birim veya modül bir akış sunuyorsa testte bir akış tarafından yayınlanan bir veya daha fazla öğeyi okuyabilir ve doğrulayabilirsiniz.
Sahte yapımcı oluşturma
Test edilen konu bir akış tüketicisi olduğunda, bunu test etmenin yaygın yollarından biri üreticiyi sahte bir uygulamayla değiştirmektir. Örneğin, üretimde iki veri kaynağından veri alan bir deposu gözlemleyen bir sınıf verildiğinde:
Testi kesin hale getirmek için deposu ve bağımlılıkları her zaman aynı sahte verileri yayınlayan sahte bir deposuyla değiştirebilirsiniz:
Bir akışta önceden tanımlanmış bir değer dizisi yayınlamak için flow
oluşturucuyu kullanın:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
Testte, gerçek uygulamanın yerine bu sahte depoyu ekleyin:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Test edilen öğenin çıkışları üzerinde kontrol sahibi olduğunuza göre, çıkışlarını kontrol ederek öğenin düzgün çalıştığını doğrulayabilirsiniz.
Testte akış emisyonlarını doğrulama
Test edilen konu bir akış sunuyorsa testin, veri akışının öğeleri hakkında iddialarda bulunması gerekir.
Önceki örneğin deposunda bir akış olduğunu varsayalım:
Belirli testlerde yalnızca ilk yayını veya akıştan gelen sınırlı sayıda öğeyi kontrol etmeniz gerekir.
first()
işlevini çağırarak akışa gönderilen ilk yayını kullanabilirsiniz. Bu işlev, ilk öğe alınana kadar bekler ve ardından iptal sinyalini üreticiye gönderir.
@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)
}
Testin birden fazla değeri kontrol etmesi gerekiyorsa toList()
çağrısı, kaynağın tüm değerlerini yayınlamasını beklemesine neden olur ve ardından bu değerleri liste olarak döndürür. Bu işlem yalnızca sonlu veri akışlarında çalışır.
@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)
}
Daha karmaşık bir öğe koleksiyonu gerektiren veya sonlu sayıda öğe döndürmeyen veri akışları için öğeleri seçip dönüştürmek üzere Flow
API'yi kullanabilirsiniz. Aşağıda bazı örnekler verilmiştir:
// 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)
Test sırasında sürekli veri toplama
Önceki örnekte görüldüğü gibi toList()
kullanarak bir akış toplamak için collect()
dahili olarak kullanılır ve sonuç listesinin tamamı döndürülmeye hazır olana kadar askıya alınır.
Akışta değer yayınlanmasına neden olan işlemleri ve yayınlanan değerlerle ilgili iddiaları art arda yerleştirmek için bir test sırasında akıştan sürekli olarak değer toplayabilirsiniz.
Örneğin, test edilecek aşağıdaki Repository
sınıfını ve test sırasında dinamik olarak değer üretmek için emit
yöntemi içeren eşlik eden sahte veri kaynağı uygulamasını ele alalım:
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
}
Bu sahte nesneyi bir testte kullanırken, Repository
kaynağından sürekli olarak değer alacak bir toplayıcı coroutine oluşturabilirsiniz. Bu örnekte, bunları bir listeye toplayıp ardından içeriğiyle ilgili iddialarda bulunuyoruz:
@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
}
Buradaki Repository
tarafından sunulan akış hiçbir zaman tamamlanmadığı için, verileri toplayan toList
çağrısı hiçbir zaman döndürülmez. Toplayıcı coroutine'u TestScope.backgroundScope
içinde başlatmak, coroutine'un testin sona ermesinden önce iptal edilmesini sağlar. Aksi takdirde runTest
, tamamlanmasını beklemeye devam eder ve testin yanıt vermeyi bırakmasına ve sonunda başarısız olmasına neden olur.
Burada toplayıcı iş parçacığı için UnconfinedTestDispatcher
değerinin nasıl kullanıldığına dikkat edin. Bu, toplayıcı iş parçacığının istekli olarak başlatılmasını ve launch
döndükten sonra değerler almaya hazır olmasını sağlar.
Turbine'i kullanma
Üçüncü taraf Turbine kitaplığı, toplayıcı bir coroutine oluşturmak için kullanışlı bir API'nin yanı sıra akışları test etmek için diğer kolaylık özelliklerini sunar:
@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())
}
}
Daha fazla ayrıntı için kitaplığın belgelerine bakın.
StateFlow'ları test etme
StateFlow
, zaman içinde tuttuğu değerleri bir akış olarak gözlemlemek için toplanabilen gözlemlenebilir bir veri tutucusudur. Bu değer akışının birleştirildiğini unutmayın. Yani değerler bir StateFlow
içinde hızlı bir şekilde ayarlanırsa bu StateFlow
'ün toplayıcılarının tüm ara değerleri almaları garanti edilmez, yalnızca en son değer alınır.
Testlerde, birleştirme işlemini göz önünde bulundurursanız Turbine dahil olmak üzere diğer tüm akışları toplayabileceğiniz gibi bir StateFlow
değerini de toplayabilirsiniz. Bazı test senaryolarında tüm ara değerleri toplamaya ve bunlarla ilgili iddiada bulunmaya çalışmak istenebilir.
Bununla birlikte, genellikle StateFlow
'ü veri sahibi olarak ele almanızı ve bunun yerine value
mülkünde iddiada bulunmanızı öneririz. Bu sayede testler, belirli bir zamanda nesnenin mevcut durumunu doğrular ve birleştirme işleminin gerçekleşip gerçekleşmediğine bağlı değildir.
Örneğin, bir Repository
'dan değer toplayan ve bunları StateFlow
içinde kullanıcı arayüzüne gösteren bu ViewModel
'ü ele alalım:
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
}
}
}
}
Bu Repository
için sahte bir uygulama şu şekilde görünebilir:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
ViewModel
öğesini bu sahte öğeyle test ederken, ViewModel
öğesinin StateFlow
alanında güncellemeleri tetiklemek için sahte öğeden değer yayınlayabilir ve ardından güncellenen value
öğesinde doğrulama yapabilirsiniz:
@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
}
stateIn tarafından oluşturulan StateFlow'larla çalışma
Önceki bölümde ViewModel
, Repository
kaynağından gelen bir akış tarafından yayınlanan son değeri depolamak için bir MutableStateFlow
kullanır. Bu, genellikle soğuk bir akışı sıcak StateFlow
'a dönüştüren stateIn
operatörü kullanılarak daha basit bir şekilde uygulanan yaygın bir kalıptır:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
stateIn
operatörünün, ne zaman etkinleşeceğini ve temel akışı ne zaman tüketmeye başlayacağını belirleyen bir SharingStarted
parametresi vardır. Görüntüleme modellerinde SharingStarted.Lazily
ve SharingStarted.WhileSubscribed
gibi seçenekler sıklıkla kullanılır.
Testinizde StateFlow
öğesinin value
özelliğini doğruluyor olsanız bile bir toplayıcı oluşturmanız gerekir. Bu, boş bir toplayıcı olabilir:
@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)
}
Ek kaynaklar
- Android'de Kotlin eşzamanlı programlarını test etme
- Android'de Kotlin akışları
StateFlow
veSharedFlow
- Kotlin coroutine'leri ve flow için ek kaynaklar