Como testar fluxos do Kotlin no Android

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 de dados na produção:

o objeto em teste e a camada de dados
Figura 1. O objeto em teste e a camada de dados.

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:

as dependências são substituídas por uma implementação fictícia
Figura 2. As dependências são substituídas por uma implementação fictícia.

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 a saída dele.

Como declarar emissões de fluxo em um teste

Se o objeto em teste estiver expondo um fluxo, ele precisará fazer declarações sobre os elementos do fluxo de dados.

Vamos supor que o repositório do exemplo anterior exponha um fluxo:

um repositório com dependências fictícias que expõem um fluxo
Figura 3. Um repositório (o objeto em teste) com dependências fictícias que expõem um fluxo.

Dependendo das necessidades do teste, você normalmente verificará 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() = runBlocking {
    // 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
    assertThat(firstItem, isEqualTo(ITEM_1) // Using AssertJ
}

Se o teste precisar verificar vários valores, chamar toList() fará com que o fluxo aguarde a fonte emitir todos os valores e, em seguida, retornará esses valores como uma lista. Isso funciona apenas para fluxos de dados finitos.

@Test
fun myRepositoryTest() = runBlocking {
    // 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
    assertThat(messages, isEqualTo(ALL_MESSAGES))
}

Para fluxos de dados que exigem uma coleção mais complexa de itens ou que 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()

// Take the first 5 distinct items
outputFlow.take(5).toSet()

// Take the first 2 items matching a predicate
outputFlow.takeWhile(predicate).take(2).toList()

// Take the first item that matches the predicate
outputFlow.firstWhile(predicate)

// Take 5 items and apply a transformation to each
outputFlow.map(transformation).take(5)

// 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)

CoroutineDispatcher como dependência

Se o objeto em teste tiver um CoroutineDispatcher como dependência, transmita uma instância do TestCoroutineDispatcher para a biblioteca kotlinx-coroutines-test e execute o corpo do teste no método runBlockingTest do agente:

private val coroutineDispatcher = TestCoroutineDispatcher()
private val uut = MyUnitUnderTest(coroutineDispatcher)

@Test
fun myTest() = coroutineDispatcher.runBlockingTest {
    // Test body
}

Recursos de fluxo adicionais