Il modo in cui testi le unità o i moduli che comunicano con flow dipende dal fatto che il soggetto sottoposto a test utilizzi o meno il flusso come input o output.
- Se il soggetto sottoposto a test osserva un flusso, puoi generare flussi all'interno con dipendenze false che puoi controllare dai test.
- Se l'unità o il modulo espone un flusso, puoi leggere e verificare uno o più elementi emessi da un flusso nel test.
Creare un finto produttore
Quando il soggetto sottoposto a test è un consumatore di un flusso, un modo comune per testarlo è la sostituzione del producer con una falsa implementazione. Ad esempio, in base a un che osserva un repository che prende i dati da due origini dati in produzione:
Per rendere deterministico il test, puoi sostituire il repository e i suoi con un repository falso che emette sempre gli stessi dati falsi:
Per emettere una serie predefinita di valori in un flusso, utilizza lo strumento per la creazione di flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
Nel test, viene inserito questo falso repository, sostituendo il repository reale implementazione:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Ora che hai il controllo sugli output del soggetto sottoposto a test, puoi e verificare che funzioni correttamente controllando gli output.
Dichiarazione delle emissioni di flusso in un test
Se il soggetto sottoposto a test sta esponendo un flusso, il test deve effettuare delle asserzioni sugli elementi dello stream di dati.
Supponiamo che il repository dell'esempio precedente mostri un flusso:
Con alcuni test, dovrai verificare solo la prima emissione o un intervallo di elementi provenienti dal flusso.
Puoi consumare la prima emissione nel flusso chiamando first()
. Questo
attende che venga ricevuto il primo elemento, quindi invia l'annullamento
segnale al produttore.
@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)
}
Se il test deve controllare più valori, la chiamata a toList()
causa il flusso
attendere che l'origine emetta tutti i valori, quindi li restituisce come
dall'elenco di lettura. Questa operazione funziona solo per flussi di dati finiti.
@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)
}
Per gli stream di dati che richiedono una raccolta più complessa di articoli o non restituiscono
un numero limitato di elementi, puoi usare l'API Flow
per scegliere e trasformare
elementi. Ecco alcuni esempi:
// 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)
Raccolta continua durante un test
La raccolta di un flusso utilizzando toList()
, come mostrato nell'esempio precedente, utilizza
collect()
internamente e sospenderlo finché l'intero elenco dei risultati non sarà pronto
restituito.
Per alternare le azioni che causano l'emissione di valori e asserzioni nel flusso valori emessi, è possibile raccogliere continuamente valori da un flusso durante un test.
Ad esempio, completa il seguente corso Repository
da testare e un
che accompagna l'implementazione di origini dati false che hanno un metodo emit
per
produrre valori in modo dinamico durante il test:
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
}
Quando utilizzi questo falso in un test, puoi creare una coroutina da collezionare che
ricevono continuamente i valori da Repository
. In questo esempio,
raccoglierle in un elenco e quindi eseguire asserzioni sui relativi contenuti:
@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
}
Poiché il flusso esposto da Repository
qui non viene mai completato, toList
che raccoglie e non restituisce mai. Inizio della raccolta della coroutine
TestScope.backgroundScope
assicura che la coroutine venga annullata prima della fine del test. Altrimenti,
runTest
avrebbe continuato ad attendere il completamento, causando l'interruzione del test
non risponde e alla fine non riesce.
Nota come
UnconfinedTestDispatcher
utilizzata per la raccolta della coroutine. Ciò garantisce che la raccolta
la coroutine è stata lanciata con entusiasmo ed è pronta a ricevere valori dopo il giorno launch
i resi.
Uso della turbina
Turbine di terze parti libreria offre una pratica API per la creazione di una coroutina, nonché Altre funzionalità utili per testare i flussi:
@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())
}
}
Consulta le documentazione della libreria per ulteriori dettagli.
Test di StateFlows
StateFlow
è un valore osservabile
titolare dei dati, che può essere raccolto per osservare i valori che conserva nel tempo
un flusso. Tieni presente che questo flusso di valori è confuso, il che significa che se
vengono impostati rapidamente in StateFlow
, i raccoglitori di questo StateFlow
non
tutti i valori intermedi, solo il più recente.
Nei test, se tieni presente la combinazione, puoi raccogliere i valori di un StateFlow
perché puoi raccogliere qualsiasi altro flusso, anche
con Turbine. Tentativo di raccolta
e affermare su tutti i valori intermedi può essere desiderabile in alcuni scenari di test.
Tuttavia, in genere consigliamo di considerare StateFlow
come titolare dei dati e
asserzione nella sua proprietà value
. In questo modo, i test convalidano
dell'oggetto in un determinato momento e non dipendono dal fatto che
avviene una combinazione.
Ad esempio, prendi questo ViewModel che raccoglie i valori da un Repository
e
e li espone alla UI in un 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
}
}
}
}
Un'implementazione falsa per questo Repository
potrebbe avere il seguente aspetto:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
Quando testi il ViewModel con questo falso, puoi emettere valori dal falso a
attiva gli aggiornamenti nell'elemento StateFlow
di ViewModel e poi applica l'oggetto
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
}
Utilizzo di StateFlow creati da stateIn
Nella sezione precedente, il ViewModel utilizza un MutableStateFlow
per archiviare
più recente valore emesso da un flusso proveniente da Repository
. Questo è un pattern comune,
di solito implementato in modo più semplice utilizzando
stateIn
che converte un flusso freddo in un StateFlow
a caldo:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
L'operatore stateIn
ha un parametro SharingStarted
, che determina quando
diventa attivo e inizia a utilizzare il flusso sottostante. Opzioni come
SharingStarted.Lazily
e SharingStarted.WhileSubsribed
sono utilizzati di frequente
in ViewModels.
Anche se le tue dichiarazioni sono sulla base di value
di StateFlow
nel tuo test,
devi creare un raccoglitore. Può essere un raccoglitore vuoto:
@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)
}
Risorse aggiuntive
- Test di coroutine Kotlin su Android
- Kotlin funziona su Android
StateFlow
eSharedFlow
- Risorse aggiuntive per coroutine e flussi di Kotlin