A forma como você testa unidades ou módulos que se comunicam com fluxos depende se o objeto em teste usa o fluxo como entrada ou saída.
- Se o objeto em teste observa um fluxo, é possível gerar fluxos em dependências fictícias que você pode controlar pelos testes.
- Se a unidade ou o módulo expõe um fluxo, é possível ler e verificar um ou vários itens emitidos por um fluxo no teste.
Como criar um produtor fictício
Quando o objeto em teste é um consumidor de um fluxo, uma maneira comum de testá-lo é substituir o produtor por uma implementação fictícia. Por exemplo, considerando uma classe que observa um repositório que usa dados de duas fontes na produção:

Para que o teste seja determinista, substitua o repositório e as dependências dele por um repositório fictício que sempre emite os mesmos dados fictícios:

Para emitir uma série predefinida de valores em um fluxo, use o builder flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
No teste, esse repositório fictício é injetado, substituindo a implementação real:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Agora que você tem controle sobre as saídas do objeto em teste, é possível verificar se ele funciona corretamente conferindo as saídas dele.
Como declarar emissões de fluxo em um teste
Se o objeto em teste estiver expondo um fluxo, o teste vai precisar fazer declarações sobre os elementos do fluxo de dados.
Vamos supor que o repositório do exemplo anterior exponha um fluxo:

Em determinados testes, é necessário verificar apenas a primeira emissão ou um número finito de itens provenientes do fluxo.
Você pode consumir a primeira emissão para o fluxo chamando first()
. Essa
função aguarda até que o primeiro item seja recebido e, em seguida, envia o sinal de
cancelamento ao produtor.
@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 o teste precisar conferir vários valores, chamar toList()
vai fazer com que o fluxo
aguarde a fonte emitir todos os valores e, em seguida, vai retornar esses valores como uma
lista. Isso funciona apenas para fluxos de dados finitos.
@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)
}
Para fluxos de dados que exigem uma coleção mais complexa de itens ou não retornam
um número finito de itens, você pode usar a API Flow
para selecionar e transformar os
itens. Veja alguns exemplos:
// 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)
Coleta contínua durante um teste
A coleta de um fluxo usando toList()
, como mostrado no exemplo anterior, usa
collect()
internamente e fica suspensa até que toda a lista de resultados esteja pronta para ser
retornada.
Para intercalar ações que fazem com que o fluxo emita valores e declarações nos valores que foram emitidos, é possível coletar continuamente valores de um fluxo durante um teste.
Por exemplo, considere a seguinte classe Repository
a ser testada e uma
implementação de origem de dados fictícia com um método emit
para
produzir valores dinamicamente durante o teste:
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
}
Ao usar essa implementação fictícia em um teste, é possível criar uma corrotina de coleta que
vai receber continuamente os valores de Repository
. Neste exemplo, estamos
coletando esses valores em uma lista e fazendo declarações no conteúdo:
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
val collectJob = launch(UnconfinedTestDispatcher()) {
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
collectJob.cancel()
}
Como o fluxo exposto pelo Repository
nunca é concluído, a chamada de
toList
que o coleta nunca é retornada. Portanto, a corrotina de coleta
precisa ser cancelada explicitamente antes do final do teste. Caso contrário,
o runTest
continuaria esperando a conclusão, fazendo com que o teste parasse
de responder e falhasse.
Observe como
UnconfinedTestDispatcher
é usado na corrotina de coleta. Isso garante que a corrotina
de coleta seja iniciada e esteja pronta para receber valores depois do retorno de
launch
.
Como usar a biblioteca Turbine
A biblioteca de terceiros Turbine oferece uma API conveniente para criar uma corrotina de coleta, bem como outros recursos práticos para testar fluxos:
@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())
}
}
Consulte a documentação da biblioteca para ver mais detalhes.
Como testar StateFlows
StateFlow
é um armazenador
de dados observáveis que pode ser coletado para observar os valores retidos ao longo do tempo como
um stream. Esse stream de valores é uma combinação, ou seja, se
os valores forem definidos em um StateFlow
rapidamente, os coletores desse StateFlow
não vão ter
garantia de receber todos os valores intermediários, apenas o mais recente.
Em testes, se você lembrar dessa característica, vai poder coletar os valores de um StateFlow
durante a coleta de qualquer outro fluxo, inclusive com a biblioteca Turbine. Tentar coletar
e declarar em todos os valores intermediários pode ser desejável em alguns cenários de teste.
No entanto, geralmente recomendamos tratar StateFlow
como um detentor de dados e
fazer as declarações na propriedade value
. Dessa forma, os testes validam o estado
atual do objeto em um determinado momento e não dependem do acontecimento
da combinação.
Por exemplo, veja este ViewModel, que coleta valores de um Repository
e
os expõe à IU em um 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
}
}
}
}
Uma implementação fictícia desse Repository
pode ter esta aparência:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
Ao testar o ViewModel com essa implementação, você pode emitir valores dela para
acionar atualizações no StateFlow
do ViewModel e, em seguida, fazer a declaração no
value
atualizado:
@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
}
Como trabalhar com StateFlows criados por stateIn
Na seção anterior, o ViewModel usa um MutableStateFlow
para armazenar o
valor mais recente emitido por um fluxo do Repository
. Esse é um padrão comum,
geralmente implementado de maneira mais simples, usando o
operador stateIn
,
que converte um fluxo frio em um StateFlow
quente:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
O operador stateIn
tem um parâmetro SharingStarted
, que determina quando
ele vai ficar ativo e começa a consumir o fluxo subjacente. Opções como
SharingStarted.Lazily
e SharingStarted.WhileSubsribed
são usadas com frequência
em ViewModels.
Mesmo que você esteja declarando no value
do StateFlow
no teste, é
necessário criar um coletor. Ele pode ser um coletor vazio:
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
val collectJob = launch(UnconfinedTestDispatcher()) { 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)
// Cancel the collecting job at the end of the test
collectJob.cancel()
}