Como testar corrotinas do Kotlin no Android

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

O código de teste de unidade que usa corrotinas exige atenção especial, já que a execução delas pode ser assíncrona e acontecer em várias linhas. Este guia aborda como as funções de suspensão podem ser testadas, as construções de teste que você precisa conhecer e como realizar testes no código que usa corrotinas.

As APIs usadas neste guia fazem parte da biblioteca kotlinx.coroutines.test. Adicione o artefato como uma dependência de teste ao projeto para ter acesso a essas APIs (links em inglês).

dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}

Como invocar funções de suspensão em testes

Para chamar funções de suspensão em testes, você precisa estar em uma corrotina. Como as funções de teste JUnit em si não são funções de suspensão, é necessário chamar um builder de corrotinas dentro dos testes para iniciar uma nova corrotina.

O runTest (link em inglês) é um builder de corrotinas projetado para testes. Use-o para unir todos os testes que incluem corrotinas. Corrotinas também podem ser iniciadas pelos objetos usados no teste.

suspend fun fetchData(): String {
    delay(1000L)
    return "Hello world"
}

@Test
fun dataShouldBeHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
}

Em geral, é necessário ter uma invocação de runTest por teste, e é recomendado usar um corpo de expressão (link em inglês).

Ao unir o código do teste com o runTest, ele vai funcionar para testar funções básicas de suspensão e ignorará automaticamente os atrasos nas corrotinas, tornando o teste acima muito mais rápido que um segundo.

No entanto, outras considerações precisam ser feitas, dependendo do que acontece no código testado:

  • Quando seu código cria novas corrotinas além da corrotina de teste de nível superior criada por runTest, você precisa escolher o TestDispatcher adequado para controlar como elas são agendadas.
  • Se o código mover a execução das corrotinas para outros agentes, por exemplo, usando withContext (link em inglês), o runTest ainda vai funcionar, mas os atrasos não serão mais ignorados, e os testes serão menos previsíveis, já que o código vai ser executado em várias linhas de execução. Por esses motivos, nos testes é preciso injetar agentes de teste para substituir agentes reais.

TestDispatchers

TestDispatchers são implementações de CoroutineDispatcher para testes (links em inglês). Você vai precisar usar TestDispatchers se novas corrotinas forem criadas durante o teste para tornar a execução das novas corrotinas previsível.

Há duas implementações disponíveis do TestDispatcher: StandardTestDispatcher e UnconfinedTestDispatcher, que executam agendamentos diferentes de corrotinas recém-iniciadas. Ambas usam um TestCoroutineScheduler para controlar o tempo virtual e gerenciar as corrotinas em execução nos testes (links em inglês).

Use apenas uma instância de agendador em um teste e a compartilhe com todos os TestDispatchers. Consulte Como injetar TestDispatchers para saber mais sobre o compartilhamento de agendadores.

Para iniciar a corrotina de teste de nível superior, runTest cria um TestScope, que é uma implementação de CoroutineScope que sempre usa um TestDispatcher (links em inglês). Se não for especificado, um TestScope vai criar um StandardTestDispatcher por padrão e o usará para executar a corrotina de teste de nível superior.

runTest monitora as corrotinas que estão na fila no agendador usado pelo agente do TestScope e não retornará enquanto houver trabalho pendente.

StandardTestDispatcher

Quando você inicia novas corrotinas em um StandardTestDispatcher, elas são enfileiradas no agendador para que sejam executadas sempre que a linha de execução de teste não tiver custo. Para permitir que essas novas corrotinas sejam executadas, libere a linha de execução de teste para o uso de outras corrotinas. Esse comportamento de enfileiramento oferece controle preciso sobre a execução de novas corrotinas durante o teste e é semelhante ao agendamento de corrotinas no código de produção.

Se a linha de execução de teste nunca for liberada durante a execução da corrotina de teste de nível superior, todas as novas corrotinas vão ser executadas somente após a conclusão da corrotina de teste (mas antes de runTest retornar):

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Há várias maneiras de liberar a corrotina de teste para permitir que as corrotinas enfileiradas sejam executadas. Todas essas chamadas permitem que outras corrotinas sejam executadas na linha de execução de teste antes de retornar:

  • advanceUntilIdle (link em inglês): executa todas as outras corrotinas no agendador até que não haja mais nada na fila. É uma boa opção padrão para executar todas as corrotinas pendentes e vai funcionar na maioria dos cenários de teste.
  • advanceTimeBy (link em inglês): avança o tempo virtual de acordo com o valor especificado e executa as corrotinas agendadas para execução antes desse momento.
  • runCurrent (link em inglês): executa as corrotinas agendadas para o tempo virtual atual.

A fim de corrigir o teste anterior, advanceUntilIdle pode ser usado para permitir que as duas corrotinas pendentes executem o trabalho antes de continuar para a declaração:

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }
    advanceUntilIdle() // Yields to perform the registrations

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

UnconfinedTestDispatcher

Quando novas corrotinas são iniciadas em um UnconfinedTestDispatcher, isso acontece prontamente na linha de execução atual. Ou seja, as corrotinas começam a ser executadas imediatamente, sem esperar o retorno do builder delas. Em muitos casos, esse comportamento resulta em um código de teste mais simples, já que não é necessário liberar manualmente a linha de execução de teste para permitir que novas corrotinas sejam executadas.

No entanto, esse comportamento é diferente do que você vai ver em produção com agentes que não são de teste. Se o teste se concentrar na simultaneidade, use StandardTestDispatcher.

Para usar esse agente na corrotina de teste de nível superior no runTest em vez do padrão, crie uma instância e a transmita como um parâmetro. Isso fará com que novas corrotinas criadas em runTest sejam executadas com rapidez, já que herdam o agente do TestScope.

@Test
fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

Neste exemplo, as chamadas vão iniciar as novas corrotinas prontamente no UnconfinedTestDispatcher, ou seja, cada chamada de inicialização só vai retornar depois que o registro for concluído.

O UnconfinedTestDispatcher inicia novas corrotinas com antecedência, mas isso não significa que elas vão ser executadas imediatamente. Se a nova corrotina for suspensa, a execução de outras corrotinas será retomada.

Por exemplo, a nova corrotina iniciada neste teste vai registrar "Alice", mas será suspensa quando delay (link em inglês) for chamado. Isso permite que a corrotina de nível superior prossiga com a declaração e o teste falhe, já que "Bob" ainda não está registrado:

@Test
fun yieldingTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch {
        userRepo.register("Alice")
        delay(10L)
        userRepo.register("Bob")
    }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Como injetar agentes de teste

O código em teste pode usar agentes para alternar entre as linhas de execução, usando withContext (link em inglês), ou iniciar novas corrotinas. Quando o código é executado em várias linhas de execução em paralelo, os testes podem ficar instáveis. Pode ser difícil fazer declarações no momento correto ou esperar que as tarefas sejam concluídas caso estejam sendo executadas em linhas de execução sem controle em segundo plano.

Nos testes, substitua esses agentes por instâncias de TestDispatchers. Essa mudança gera vários benefícios:

  • O código vai ser executado na única linha de execução de teste, tornando os testes mais determinísticos.
  • Você vai poder controlar como as novas corrotinas são agendadas e executadas.
  • Os TestDispatchers usam um agendador de tempo virtual, que ignora atrasos automaticamente e permite que você avance manualmente o tempo.

O uso da injeção de dependência para fornecer agentes às suas classes facilita a substituição de agentes reais em testes.

Por padrão, os TestDispatchers criam um novo agendador quando instanciados. Dentro de runTest, você pode acessar a propriedade testScheduler do TestScope e transmiti-la para quaisquer TestDispatchers recém-criados. Isso vai compartilhar o reconhecimento do tempo virtual dos agentes. Métodos como o advanceUntilIdle vão executar corrotinas em todos os agentes de teste até a conclusão.

No exemplo abaixo, você pode ver uma classe Repository que cria uma nova corrotina usando o agente IO no método initialize e alterna o autor da chamada para o agente IO no método fetchData:

// Example class demonstrating dispatcher use cases
class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)
    val initialized = AtomicBoolean(false)

    // A function that starts a new coroutine on the IO dispatcher
    fun initialize() {
        scope.launch {
            initialized.set(true)
        }
    }

    // A suspending function that switches to the IO dispatcher
    suspend fun fetchData(): String = withContext(ioDispatcher) {
        require(initialized.get()) { "Repository should be initialized first" }
        delay(500L)
        "Hello world"
    }
}

Nos testes, é possível injetar uma implementação de TestDispatcher para substituir o agente IO.

No exemplo abaixo, injetamos um StandardTestDispatcher no repositório e usamos advanceUntilIdle para garantir que a nova corrotina iniciada em initialize seja concluída antes de continuar.

O fetchData também vai se beneficiar da execução de TestDispatcher, já que ele vai ser executado na linha de execução de teste e ignorará o próprio atraso durante o teste.

class RepositoryTest {
    @Test
    fun repoInitWorksAndDataIsHelloWorld() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val repository = Repository(dispatcher)

        repository.initialize()
        advanceUntilIdle() // Runs the new coroutine
        assertEquals(true, repository.initialized.get())

        val data = repository.fetchData() // No thread switch, delay is skipped
        assertEquals("Hello world", data)
    }
}

Novas corrotinas iniciadas em um TestDispatcher podem ser avançadas manualmente, como mostrado acima, com initialize. No entanto, isso não seria possível ou desejável no código de produção. Em vez disso, esse método precisa ser reformulado para ser de suspensão (para execução sequencial) ou retornar um valor Deferred (para execução simultânea).

Por exemplo, você pode usar async para iniciar uma nova corrotina e criar um Deferred (links em inglês):

class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)

    fun initialize() = scope.async {
        // ...
    }
}

Isso permite que você await (espere) a conclusão deste código com segurança nos testes e no código de produção:

@Test
fun repoInitWorks() = runTest {
    val dispatcher = StandardTestDispatcher(testScheduler)
    val repository = BetterRepository(dispatcher)

    repository.initialize().await() // Suspends until the new coroutine is done
    assertEquals(true, repository.initialized.get())
    // ...
}

O runTest vai aguardar a conclusão das corrotinas pendentes antes de retornar se elas estiverem em um TestDispatcher compartilhado com um agendador. Ele vai aguardar também as corrotinas filhas da corrotina de teste de nível superior, mesmo que estejam em outros agentes (até um tempo limite especificado pelo parâmetro dispatchTimeoutMs, que é de 60 segundos por padrão).

Como configurar o agente Main

Em testes de unidade locais, o agente Main que envolve a linha de execução de IU do Android vai ficar indisponível, já que esses testes são executados em uma JVM local, e não em um dispositivo Android. Se o código em teste referenciar a linha de execução principal, uma exceção vai ser gerada durante os testes de unidade.

Em alguns casos, é possível injetar o agente Main da mesma forma que outros, conforme descrito na seção anterior, permitindo que você o substitua por um TestDispatcher em testes. No entanto, algumas APIs, como viewModelScope, usam internamente um agente Main fixado no código.

Veja um exemplo de implementação do ViewModel que usa o viewModelScope para iniciar uma corrotina que carrega dados:

class HomeViewModel : ViewModel() {
    private val _message = MutableStateFlow("")
    val message: StateFlow<String> get() = _message

    fun loadMessage() {
        viewModelScope.launch {
            _message.value = "Greetings!"
        }
    }
}

Para substituir o agente Main por um TestDispatcher em todos os casos, use as funções Dispatchers.setMain e Dispatchers.resetMain (links em inglês).

class HomeViewModelTest {
    @Test
    fun settingMainDispatcher() = runTest {
        val testDispatcher = UnconfinedTestDispatcher(testScheduler)
        Dispatchers.setMain(testDispatcher)

        try {
            val viewModel = HomeViewModel()
            viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly
            assertEquals("Greetings!", viewModel.message.value)
        } finally {
            Dispatchers.resetMain()
        }
    }
}

Se o agente Main for substituído por um TestDispatcher, todos os TestDispatchers recém-criados vão usar automaticamente o agendador do agente Main, incluindo o StandardTestDispatcher criado por runTest, se nenhum outro agente for transmitido a ele.

Isso facilita o uso de um único agendador durante o teste. Para que isso funcione, crie todas as outras instâncias de TestDispatcher depois de chamar Dispatchers.setMain.

Um padrão comum para evitar a duplicação do código que substitui o agente Main em cada teste é extraí-lo para uma regra de teste do JUnit (link em inglês):

// Reusable JUnit4 TestRule to override the Main dispatcher
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun settingMainDispatcher() = runTest { // Uses Main’s scheduler
        val viewModel = HomeViewModel()
        viewModel.loadMessage()
        assertEquals("Greetings!", viewModel.message.value)
    }
}

Essa implementação de regra usa um UnconfinedTestDispatcher por padrão, mas um StandardTestDispatcher pode ser transmitido como um parâmetro se o agente Main não puder ser executado com antecedência em uma determinada classe de teste.

Quando você precisar de uma instância do TestDispatcher no corpo do teste, use novamente o testDispatcher da regra, desde que ele seja do tipo pretendido. Se quiser deixar claro o tipo de TestDispatcher usado no teste ou se precisar de um TestDispatcher diferente do tipo usado para Main, crie um novo TestDispatcher em runTest. Como o agente Main está definido como um TestDispatcher, todos os TestDispatchers recém-criados podem compartilhar o agendador automaticamente.

class DispatcherTypesTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler
        // Use the UnconfinedTestDispatcher from the Main dispatcher
        val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher)

        // Create a new StandardTestDispatcher (uses Main’s scheduler)
        val standardRepo = Repository(StandardTestDispatcher())
    }
}

Como criar agentes fora de um teste

Em alguns casos, pode ser necessário disponibilizar um TestDispatcher fora do método de teste. Por exemplo, durante a inicialização de uma propriedade na classe de teste:

class Repository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ }

class RepositoryTestWithRule {
    private val repository = Repository(/* What TestDispatcher? */)

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun someRepositoryTest() = runTest {
        // Test the repository...
    }
}

Se você estiver substituindo o agente Main como mostrado na seção anterior, TestDispatchers criados após a substituição do Main vão compartilhar o agendador automaticamente.

No entanto, esse não é o caso dos TestDispatchers criados como propriedades da classe de teste ou TestDispatchers criados durante a inicialização das propriedades. Eles são inicializados antes da substituição do agente Main. Sendo assim, eles criam novos agendadores.

Para garantir que haja apenas um agendador no teste, crie a propriedade MainDispatcherRule antes de qualquer outra. Em seguida, reutilize o agente (ou o agendador, se precisar de um TestDispatcher de tipo diferente) nos inicializadores de outras propriedades da classe, conforme necessário.

class RepositoryTestWithRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val repository = Repository(mainDispatcherRule.testDispatcher)

    @Test
    fun someRepositoryTest() = runTest { // Takes scheduler from Main
        // Any TestDispatcher created here also takes the scheduler from Main
        val newTestDispatcher = StandardTestDispatcher()

        // Test the repository...
    }
}

Tanto o runTest quanto os TestDispatchers criados no teste ainda vão compartilhar automaticamente o agendador do agente Main.

Se você não estiver substituindo o agente Main, crie seu primeiro TestDispatcher (que cria um novo agendador) como uma propriedade da classe. Em seguida, transmita manualmente esse agendador para cada invocação do runTest e cada novo TestDispatcher criado, tanto como propriedades quanto dentro do teste:

class RepositoryTest {
    // Creates the single test scheduler
    private val testDispatcher = UnconfinedTestDispatcher()
    private val repository = Repository(testDispatcher)

    @Test
    fun someRepositoryTest() = runTest(testDispatcher.scheduler) {
        // Take the scheduler from the TestScope
        val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler)
        // Or take the scheduler from the first dispatcher, they’re the same
        val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler)

        // Test the repository...
    }
}

Neste exemplo, o agendador do primeiro agente é transmitido ao runTest. Isso cria um novo StandardTestDispatcher para o TestScope usando o mesmo agendador. Também é possível transmitir o agente diretamente ao runTest para executar internamente a corrotina de teste.

Como criar seu próprio TestScope

Assim como em TestDispatchers, pode ser necessário acessar um TestScope fora do corpo do teste. Embora o runTest crie um TestScope internamente de forma automática, você também pode criar seu próprio TestScope para usar com o runTest.

Ao fazer isso, não se esqueça de chamar runTest no TestScope que você criou:

class SimpleExampleTest {
    val testScope = TestScope() // Creates a StandardTestDispatcher

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

O código acima cria um StandardTestDispatcher para o TestScope implicitamente, bem como um novo agendador. Esses objetos também podem ser criados explicitamente. Isso pode ser útil quando você precisar fazer a integração com configurações de injeção de dependência.

class ExampleTest {
    val testScheduler = TestCoroutineScheduler()
    val testDispatcher = StandardTestDispatcher(testScheduler)
    val testScope = TestScope(testDispatcher)

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

Outros recursos