Akış ile iletişim kuran birimleri veya modülleri test etme şekliniz, test edilen kişinin akışı giriş veya çıkış olarak kullanıp kullanmamasına bağlıdır.
- Test edilen özne bir akış gözlemlerse testlerle kontrol edebileceğiniz sahte bağımlılıklar dahilinde akışlar oluşturabilirsiniz.
- Birim veya modül bir akış ortaya çıkarsa testteki bir akış tarafından yayınlanan bir veya daha fazla öğeyi okuyup doğrulayabilirsiniz.
Sahte yapımcı oluşturma
Test edilen kişi bir akışın tüketicisi olduğunda bunu test etmenin yaygın yollarından biri, üreticiyi sahte bir uygulamayla değiştirmektir. Örneğin, üretim sırasında iki veri kaynağından veri alan bir depoyu gözlemleyen bir sınıfa bakalım:
Testi belirleyici hale getirmek için depoyu ve bağımlılıklarını, her zaman aynı sahte verileri yayınlayan sahte bir kod 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 yerini alan bu sahte depo yerleştirilir:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Artık test edilen öznenin çıktılarını kontrol ettiğinize göre, çıktılarını kontrol ederek doğru şekilde çalıştığını doğrulayabilirsiniz.
Bir testte akış emisyonlarını iddia etme
Test edilen özne bir akışı açığa çıkarıyorsa testin, veri akışının öğeleri hakkında iddialarda bulunması gerekir.
Önceki örneğin deposunun bir akış ortaya çıkardığını varsayalım:
Bazı testlerde, akıştan gelen ilk emisyonu veya sınırlı sayıda öğeyi kontrol etmeniz gerekir.
first()
yöntemini çağırarak akışa giden ilk emisyonu tüketebilirsiniz. Bu işlev, ilk öğe alınana kadar bekler ve ardından üreticiye iptal sinyalini 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 çok değeri kontrol etmesi gerekiyorsa toList()
yöntemini çağırmak, akışın 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 özellik yalnızca sınırlı 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 sınırlı sayıda öğe döndürmeyen veri akışlarında, öğeleri seçmek ve dönüştürmek için Flow
API'yi kullanabilirsiniz. Bazı örnekler:
// 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)
Bir test sırasında sürekli olarak toplama
Önceki örnekte görüldüğü gibi toList()
kullanarak bir akışın toplanması, dahili olarak collect()
kullanır ve sonuç listesinin tamamı döndürülmeye hazır olana kadar askıya alır.
Akışın yayınlanan değerlerde değer ve onaylar yaymasına neden olan işlemlere araya boşluk eklemek için test sırasında sürekli olarak bir akıştan değerler toplayabilirsiniz.
Örneğin, test edilecek aşağıdaki Repository
sınıfını ve test sırasında değerleri dinamik olarak oluşturmak için emit
yöntemi içeren sahte bir 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 öğeyi bir testte kullanırken, değerleri sürekli olarak Repository
öğesinden alacak bir toplama eş yordamı oluşturabilirsiniz. Bu örnekte, bu öğeleri bir listede topladık ve içeriklerine dair iddialar gerçekleştireceğiz:
@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
}
Burada Repository
tarafından açığa çıkarılan akış hiçbir zaman tamamlanmadığından bu akışı toplayan toList
çağrısı hiçbir zaman geri dönmez. Ortak programın TestScope.backgroundScope
üzerinden toplanması, test bitmeden önce eş yordamın iptal edilmesini sağlar. Aksi takdirde runTest
, işlemin tamamlanmasını beklemeye devam eder ve testin yanıt vermeyi bırakmasına ve sonunda başarısız olur.
Buradaki toplama eş yordamı için UnconfinedTestDispatcher
ürününün nasıl kullanıldığına dikkat edin. Bu, toplama eş yordasının istekle başlatılmasını ve launch
döndürüldükten sonra değerleri almaya hazır olmasını sağlar.
Türbin Kullanımı
Üçüncü taraf Türbin kitaplığı, toplama eş yordamı oluşturmak için kullanışlı bir API'nin yanı sıra Akışları test etmeye yönelik diğer kullanışlı özellikler 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 bilgi için kitaplık belgelerine bakın.
StateFlow'ları test etme
StateFlow
gözlemlenebilir bir veri sahibidir. Bu veri, zaman içinde akış olarak sahip olduğu değerleri gözlemlemek için toplanabilir. Bu değer akışının birleştirildiğini unutmayın. Bu, değerler bir StateFlow
içinde hızlı bir şekilde ayarlanırsa bu StateFlow
toplayıcılarının tüm ara değerleri, yalnızca en yeni değeri alacağının garanti edilmediği anlamına gelir.
Testlerde, birleştirmeyi göz önünde bulundurursanız Türbin de dahil olmak üzere diğer akışları toplayabileceğiniz için bir StateFlow
değerlerini toplayabilirsiniz. Bazı test senaryolarında, tüm ara değerleri toplamaya ve hak iddia etmeye çalışmak istenebilir.
Ancak genellikle StateFlow
öğesini veri sahibi olarak değerlendirmenizi ve bunun yerine value
mülkü için hak talebinde bulunmanızı öneririz. Bu şekilde, testler nesnenin belirli bir zamandaki mevcut durumunu doğrular ve birleştirmenin gerçekleşip gerçekleşmemesine bağlı olmaz.
Örneğin, Repository
öğesinden değerler toplayan ve bunları StateFlow
içinde kullanıcı arayüzüne gösteren şu ViewModel'i 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 aşağıdaki gibi 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'i bu sahte öğe ile test ederken, ViewModel'in StateFlow
içindeki güncellemeleri tetiklemek için sahte değerden değerler verebilir ve ardından, güncellenen value
öğesine onay verebilirsiniz:
@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 StateFlows ile çalışma
Önceki bölümde ViewModel, Repository
akışının yaydığı en son değeri depolamak için bir MutableStateFlow
kullanır. Bu, soğuk akışı sıcak StateFlow
değerine dönüştüren stateIn
operatörü kullanılarak genellikle 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ü, ne zaman etkinleşeceğini belirleyen ve temel akışı tüketmeye başlayan bir SharingStarted
parametresine sahiptir. SharingStarted.Lazily
ve SharingStarted.WhileSubsribed
gibi seçenekler, ViewModels'de sıkça kullanılır.
Testinizde StateFlow
öğesinin value
üzerinde hak iddia ediyor 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ş yordamlarını test etme
- Android'de Kotlin akışları
StateFlow
veSharedFlow
- Kotlin eş yordamları ve akışı için ek kaynaklar