Akışla iletişim kuran birimleri veya modülleri test etme şekliniz, test edilen öğenin 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ış gösteriyorsa testte akış tarafından yayınlanan bir veya daha fazla öğeyi okuyup doğrulayabilirsiniz.
Sahte yapımcı oluşturma
Test edilen konu bir akışın tüketicisi olduğunda, bunu test etmenin yaygın bir yolu üreticiyi sahte bir uygulamayla değiştirmektir. Örneğin, üretimde iki veri kaynağından veri alan bir depoyu gözlemleyen bir sınıf verildiğinde:
Testin deterministik olması için depoyu ve bağımlılıklarını her zaman aynı sahte verileri yayan sahte bir depo ile 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 öğenin çıkışlarını kontrol edebildiğinize göre, çıkışlarını kontrol ederek öğenin doğru şekilde çalıştığını doğrulayabilirsiniz.
Hilt test kılavuzuna bakın.Testte akış emisyonlarını onaylama
Test edilen konu bir akış gösteriyorsa testin, veri akışının öğeleri üzerinde onaylamalar yapması gerekir.
Önceki örnekteki depoda bir akışın kullanıma sunulduğunu varsayalım:
Belirli testlerde yalnızca ilk emisyonu veya akıştan gelen sınırlı sayıda öğeyi kontrol etmeniz gerekir.
first() numaralı telefonu arayarak akışa yapılan 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() işlevinin çağrılması, akışın kaynağın tüm değerlerini yayınlamasını beklemesine ve ardından bu değerleri liste olarak döndürmesine neden olur. Bu işlem yalnızca sınırlı veri akışlarında çalışır.
@Test fun myRepositoryTestList() = runTest { val repository = MyFakeRepository() // 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ı için öğeleri seçip dönüştürmek üzere Flow API'sini 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 toplama
Önceki örnekte gösterildiği gibi toList() kullanılarak bir akış toplandığında dahili olarak collect() kullanılır ve sonuç listesinin tamamı döndürülmeye hazır olana kadar askıya alınır.
Akışın değerler ve onaylar yayınlamasına neden olan işlemleri iç içe geçirmek için test sırasında bir 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 değerleri dinamik olarak oluşturmak için emit yöntemine sahip, eşlik eden 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 sahteyi bir testte kullanırken Repository değerlerini sürekli olarak alacak bir toplama eş yordamı oluşturabilirsiniz. Bu örnekte, bunları bir listede topluyor ve ardından içeriğiyle ilgili onaylamalar yapıyoruz:
@OptIn(ExperimentalCoroutinesApi::class) @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 kullanıma sunulan akış hiçbir zaman tamamlanmadığı için toList
onu toplayan çağrı hiçbir zaman geri dönmez. TestScope.backgroundScope içinde toplama eşzamanlı yordamının başlatılması, eşzamanlı yordamın testin sonunda iptal edilmesini sağlar. Aksi takdirde, runTest tamamlanmayı beklemeye devam eder ve testin yanıt vermeyi bırakmasına, sonunda da başarısız olmasına neden olur.
Burada toplama eşzamanlı işlevi için nasıl
UnconfinedTestDispatcher
kullanıldığına dikkat edin. Bu, toplama yardımcı yordamının hemen başlatılmasını ve launch döndürüldükten sonra değerleri almaya hazır olmasını sağlar.
Turbine'i kullanma
Üçüncü taraf Turbine kitaplığı, toplama işlevi oluşturmak için kullanışlı bir API'nin yanı sıra Flow'ları test etmeye yönelik diğer kolaylık özelliklerini de 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çindeki değerlerini akış olarak gözlemlemek için toplanabilen, gözlemlenebilir bir veri tutucudur. Bu değer akışının birleştirildiğini unutmayın. Bu nedenle, değerler StateFlow içinde hızlı bir şekilde ayarlanırsa bu StateFlow değerlerinin toplayıcılarının tüm ara değerleri değil, yalnızca en son değeri alması garanti edilir.
Testlerde, birleştirme işlemini göz önünde bulundurursanız Turbine dahil olmak üzere diğer tüm akışlarda olduğu gibi StateFlow değerlerini de toplayabilirsiniz. Bazı test senaryolarında tüm ara değerleri toplamaya ve onaylamaya çalışmak istenebilir.
Ancak genellikle StateFlow öğesini bir veri sahibi olarak ele almanızı ve bunun yerine value mülkünde onaylama yapmanızı öneririz. Bu sayede testler, nesnenin belirli bir zamandaki mevcut durumunu doğrular ve birleştirmenin gerçekleşip gerçekleşmediğine bağlı olmaz.
Örneğin, Repository öğesinden değerler toplayan ve bunları StateFlow öğesinde kullanıcı arayüzüne sunan şu ViewModel öğesini 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 ile test ederken sahteden değerler yayınlayarak ViewModel öğesinin StateFlow bölümünde güncellemeleri tetikleyebilir ve ardından güncellenen value üzerinde onaylama 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'daki bir akış tarafından yayılan en son değeri depolamak için MutableStateFlow kullanır. Bu, genellikle stateIn operatörü kullanılarak daha basit bir şekilde uygulanan yaygın bir kalıptır. Bu operatör, soğuk akışı sıcak bir StateFlow'ye dönüştürür:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() { val score: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) }
stateIn operatörünün, etkinleşeceği ve temel akışı kullanmaya başlayacağı zamanı belirleyen bir SharingStarted parametresi vardır. SharingStarted.Lazily ve SharingStarted.WhileSubscribed gibi seçenekler, görünüm modellerinde sıklıkla kullanılır.
Testinizde value StateFlow üzerinde onaylama yapsanız bile bir toplayıcı oluşturmanız gerekir. Bu, boş bir toplayıcı olabilir:
@OptIn(ExperimentalCoroutinesApi::class) @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ı
StateFlowveSharedFlow- Kotlin coroutines ve akışıyla ilgili ek kaynaklar