1 – Introdução

Testar código assíncrono é, muitas vezes, um desafio. Funções que utilizam corrotinas podem ter comportamento imprevisível devido a atrasos, execução concorrente e mudanças de contexto. Felizmente, o Kotlin oferece ferramentas nativas e bibliotecas auxiliares para simplificar os testes unitários com corrotinas.

Neste artigo, exploraremos como usar ferramentas como UnconfinedTestDispatcher, TestCoroutineScheduler, e a biblioteca Turbine para testar corrotinas e fluxos de forma eficiente.


2 – O Problema de Testar Código Assíncrono

Testar código assíncrono é complicado porque:

  1. Corrotinas podem ser executadas em diferentes threads ou contextos.
  2. Métodos como delay ou withTimeout introduzem esperas reais que podem tornar os testes lentos.
  3. Fluxos (Flow) dependem de eventos assíncronos que precisam ser controlados.

Sem as ferramentas corretas, testes podem se tornar inconsistentes (flaky) ou levar mais tempo do que o necessário.


3 – Ferramentas Nativas do Kotlin

3.1 – UnconfinedTestDispatcher

  • O que é? Um dispatcher projetado para testes que não está vinculado a threads específicas.
  • Por que usar? Permite executar corrotinas sem restrições de contexto, tornando os testes previsíveis.
  • Exemplo:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

fun main() = runTest {
    // O testScheduler é um TestCoroutineScheduler fornecido automaticamente pelo runTest.
    // Ele gerencia o tempo virtual para todas as corrotinas neste teste.
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)

    // Utilizamos o testDispatcher para garantir que todas as operações compartilhem além do mesmo scheduler, também o mesmo dispatcher.
    withContext(testDispatcher) {
        println("Executando no UnconfinedTestDispatcher: ${Thread.currentThread().name}")
        delay(1000) // Simula um atraso de 1 segundo virtual
        println("Finalizando no UnconfinedTestDispatcher.")
    }
}

3.2 – TestCoroutineScheduler

  • O que é? Um agendador que permite avançar o tempo manualmente em testes.
  • Por que usar? Para controlar tarefas baseadas em tempo (delay, withTimeout) sem esperar o tempo real passar.
  • Exemplo:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.Test

class TestesCoroutines {
    // Cria um TestCoroutineScheduler manualmente
    private val scheduler = TestCoroutineScheduler()

    // Cria um dispatcher baseado no scheduler manual
    private var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(scheduler)

    @Test
    fun testCoroutines() {
        // Usa runTest com o dispatcher configurado
        runTest(testDispatcher) {
            println("Tarefa iniciada.")

            // Lança uma corrotina no mesmo dispatcher
            delay(1000) // Simula um atraso de 1 segundo virtual
            println("Tarefa concluída.")

            // Avança o tempo virtualmente em 1 segundo
            scheduler.advanceTimeBy(1000)
        }
    }
}

Por que precisamos do @Test?

  • A anotação @Test é usada para marcar um método como um caso de teste, permitindo que ele seja executado pelo JUnit.
  • Sem o @Test, o método testCoroutines seria tratado como um método normal e não apareceria como executável no IDE.
  • Quando o JUnit detecta o @Test, ele:
    1. Instancia automaticamente a classe de teste.
    2. Executa o método marcado, com todas as dependências configuradas (como scheduler e testDispatcher).

3.3 – runTest

  • O que é? Uma função que fornece um ambiente controlado para executar corrotinas em testes.
  • Por que usar? Simplifica a configuração de testes assíncronos, substituindo runBlocking em testes.
  • Exemplo:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

fun main() = runTest {
    println("Iniciando o teste.")
    delay(1000) // Isso não atrasa o teste real
    println("Teste finalizado.")
}

4 - Testando fluxos com a biblioteca turbine

Para fluxos (Flow), a biblioteca Turbine simplifica a coleta e validação de valores emitidos.

4.1 – O que é Turbine?

Turbine é uma biblioteca projetada especificamente para testar fluxos no Kotlin, permitindo validar os itens emitidos e eventos como cancelamento ou conclusão.

  • Exemplo com turbine:
import app.cash.turbine.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest

fun main() = runTest {
    val fluxo = flow {
        emit(1)
        delay(1000)
        emit(2)
    }

    fluxo.test {
        val item1 = awaitItem()
        println("Recebido: $item1") // Exibe o primeiro valor no terminal
        assert(item1 == 1) // Valida o primeiro item

        val item2 = awaitItem()
        println("Recebido: $item2") // Exibe o segundo valor no terminal
        assert(item2 == 2) // Valida o segundo item

        awaitComplete() // Confirma que o fluxo foi concluído
        println("Fluxo concluido!") // Indica que o fluxo terminou
    }
}

5 – Comparação de Ferramentas

Ferramenta Função Quando usar
UnconfinedTestDispatcher Executa corrotinas sem restrições de contexto. Testes simples que não dependem de tempo real.
TestCoroutineScheduler Controla tempo manualmente em testes. Testes com delay, timeout, ou eventos de tempo.
Turbine Testa valores emitidos por Flow. Testes específicos de fluxos.

6 – Conclusão

Testar corrotinas e fluxos no Kotlin pode ser desafiador, mas com as ferramentas certas, é possível criar testes rápidos, eficientes e confiáveis. O uso de UnconfinedTestDispatcher, TestCoroutineScheduler, e bibliotecas como o Turbine torna o processo muito mais simples.

Resumo:

  1. Controle de tempo: Use TestCoroutineScheduler para gerenciar atrasos e agendamentos.
  2. Ambientes de teste: runTest é a base para testes assíncronos.
  3. Testes de fluxo: Turbine é a solução ideal para validar fluxos.

Agora que você conhece as melhores práticas para testar corrotinas no Kotlin, experimente aplicá-las nos seus projetos!

No próximo artigo, iremos discorrer um pouquinho mais a respeito de exemplos de assincronia, somente para explorarmos o assunto um pouco mais e fixarmos o conteúdo melhor.