Android'de Kotlin akışlarını test etme

Akış ile iletişim kuran birimleri veya modülleri test etme şekliniz bu, test edilen kişinin akışı giriş mi yoksa çıkış olarak mı kullandığına bağlıdır.

  • Test edilen kişi bir akış gözlemlerse, tablodaki kontrol edebileceğiniz sahte bağımlılıkları ifade eder.
  • Birim veya modül bir akış ortaya çıkarsa, bunlardan birini okuyup doğrulayabilirsiniz. testte bir akış tarafından yayınlanan birden çok öğe.
ziyaret edin.

Sahte bir yapımcı oluşturma

Test edilen kişi bir akışın tüketicisi olduğunda, akış test etmek için sık kullanılan yöntemlerden biridir. yapımcıyı sahte bir uygulamayla değiştirmektir. Örneğin, iki veri kaynağından veri alan depoyu gözlemleyen sınıf üretim:

test edilen kişi ve veri katmanı
Şekil 1. Test edilen kişi ve veriler katmanıdır.

Testi belirleyici hale getirmek için depoyu ve depoyu her zaman aynı sahte verileri yayan sahte bir depoya sahip bağımlılıklar:

bağımlılıklar sahte bir uygulamayla değiştirilir
Şekil 2. Bağımlılıklar sahte bir hakkında daha fazla bilgi edinin.

Bir akışta önceden tanımlanmış bir değer serisi yayınlamak için flow oluşturucuyu kullanın:

class MyFakeRepository : MyRepository {
    fun observeCount() = flow {
        emit(ITEM_1)
    }
}

Testte bu sahte depo yerleştirilir ve gerçek deponun yerini alır. uygulama:

@Test
fun myTest() {
    // Given a class with fake dependencies:
    val sut = MyUnitUnderTest(MyFakeRepository())
    // Trigger and verify
    ...
}

Artık test edilen konunun çıktıları üzerinde kontrol sahibi olduğunuza göre, çıkışlarını kontrol ederek düzgün şekilde çalıştığını doğrulayın.

Bir testte akış emisyonlarını iddia etme

Test edilen kişi bir akıntıyı ortaya çıkarıyorsa testin onaylamada bulunması gerekir. oluşturmaya karar veriyor.

Bir önceki örneğin deposunun bir akışı ortaya çıkardığını varsayalım:

akış başlatan sahte bağımlılıklar içeren depo
Şekil 3. Sahte içerik içeren bir depo (test edilen kişi) bağımlılıkları ortaya koyabilir.

Belirli testlerde sadece ilk emisyonu veya sonlu akıştan gelen öğe sayısını ifade eder.

Akışa yönelik ilk emisyonu, first() yöntemini çağırarak tüketebilirsiniz. Bu fonksiyon ilk öğe alınıncaya kadar bekler ve ardından iptal isteğini gönderir yapımcıya ilettiğim.

@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() işlevinin çağrılması akışa neden olur için kaynağın tüm değerlerini yayınlamasını beklemek, sonra da bu değerleri bir liste'ye dokunun. Bu 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 dönmeyen veri akışları için sınırlı sayıda öğe varsa seçmek ve dönüştürmek için Flow API'yi öğeler. 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örüldüğü gibi toList() kullanarak akış toplama Dahili olarak collect() oluşturur ve tüm sonuç listesi hazır olana kadar askıya alınır geri döndü.

Akışın için bir akıştan sürekli olarak değer toplayarak gerekir.

Örneğin, test edilecek aşağıdaki Repository sınıfını ve bir emit yöntemi olan sahte veri kaynağı uygulamasıyla ilgili bir değer test sırasında dinamik olarak üretilir:

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 işlemi bir testte kullanırken, A/B testinin arka planındaki Repository kaynaklı değerleri sürekli olarak alır. Bu örnekte bir liste halinde toplayıp bu e-postanın içeriklerinde onaylamalar yapacaksınız.

@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 gösterilen akış hiçbir zaman tamamlanmadığı için toList çağrısının geri dönüşü yoktur. Koordinat toplama işlemine şu tarihte başlanıyor: TestScope.backgroundScope Eş yordamın test bitmeden iptal edilmesini sağlar. Aksi halde runTest, testin tamamlanmasını beklemeye devam ederek testin durmasına neden oluyor ve sonunda başarısız olur.

Nasıl olduğuna dikkat edin UnconfinedTestDispatcher Buradaki eş yordam toplama işlemi için kullanılır. Bu sayede, toplanan veriler eş yordam heyecanla kullanıma sunuldu ve launch tarihinden sonra değer almaya hazır belirtir.

Türbin Kullanma

Üçüncü taraf Türbin kitaplığının, toplama koordinasyonu oluşturmak için kullanışlı bir API'si de vardır. test etmek için başka kolaylık özellikleri olarak görebilirsiniz:

@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())
    }
}

Bkz. kitaplığının dokümanlarına göz atın inceleyebilirsiniz.

StateFlows'u test etme

StateFlow gözlemlenebilir bir toplanabilir ve böylece zaman içinde sahip olduğu değerleri gözlemlemek için toplanabilir. bir deneyimdir. Bu değer akışının karıştırıldığını unutmayın. Bu, değerler StateFlow içinde hızlı bir şekilde ayarlanırsa StateFlow bu toplayıcıların toplayıcıları değil Yalnızca en güncel olanı olmak üzere tüm ara değerleri alması garanti edilir.

Testlerde, karmayı aklınızda tutarsanız StateFlow değerlerini toplayabilirsiniz gibi başka akışlar da toplayabilirsiniz. Toplanmaya çalışılıyor ve tüm ara değerler üzerinde hak iddia etmek, bazı test senaryolarında tercih edilebilir.

Ancak genellikle StateFlow öğesini veri sahibi olarak ele almanızı ve bunun yerine value mülkünde yer alıyor. Bu şekilde, testler mevcut nesnenin belirli bir zamandaki durumundadır ve bağlı olup olmamasına bağlı değildir çakışmaları yaşanır.

Örneğin, bir Repository ve bunları bir StateFlow içinde kullanıcı arayüzüne gösterir:

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'i bu sahte ile test ederken sahte değerden ViewModel'in StateFlow öğesinde güncellemeleri tetikler ve ardından, güncellenen 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
}

stateIn tarafından oluşturulan StateFlows ile çalışma

Önceki bölümde ViewModel, dönüşüm verilerini depolamak için bir MutableStateFlow Repository kaynaklı bir akışın yayınladığı en son değer. Bu yaygın bir modeldir daha basit bir şekilde uygulanır. stateIn operatörü, soğuk akışı sıcak bir StateFlow parametresine 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ü, SharingStarted parametresi içerir. Bu parametre, devreye girer ve temel akışı tüketmeye başlar. Şunun gibi seçenekler: SharingStarted.Lazily ve SharingStarted.WhileSubsribed sık kullanılıyor ViewModelleri'nde.

Testinizde StateFlow öğesinin value üzerinde hak talebinde bulunsanız bile 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