Como testar corrotinas do Kotlin no Android

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.

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 vai 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 dispatchers, 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. Por esses motivos, é preciso injetar dispatchers de teste para substituir os reais quando estiver testando.

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 de TestDispatcher: StandardTestDispatcher e UnconfinedTestDispatcher, que realizam um agendamento diferente de corrotinas recém-iniciadas. Ambos usam um TestCoroutineScheduler para controlar o tempo virtual e gerenciar as corrotinas em execução em um teste.

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 dispatcher 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 dispatchers que não são de teste. Se o teste se concentrar na simultaneidade, use StandardTestDispatcher.

Para usar esse dispatcher na corrotina de teste de nível superior no runTest em vez do padrão, crie uma instância e transmita-a como um parâmetro. Isso fará com que novas corrotinas criadas em runTest sejam executadas com rapidez, já que herdam o dispatcher 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 dispatchers de teste

O código em teste pode usar dispatchers 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 dispatchers 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 deterministas.
  • 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 dispatchers às suas classes facilita a substituição de dispatchers reais em testes. Nesses exemplos, injetaremos um CoroutineDispatcher, mas também é possível injetar o tipo CoroutineContext (link em inglês) mais amplo, o que permite ainda mais flexibilidade durante testes.

Para classes que iniciam corrotinas, também é possível injetar um CoroutineScope em vez de um dispatcher, conforme detalhado na seção Como injetar um escopo.

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 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 dispatchers de teste até a conclusão.

No exemplo abaixo, você pode conferir uma classe Repository, que cria uma nova corrotina usando o dispatcher IO no método initialize e alterna o autor da chamada para o dispatcher 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 dispatcher 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 dispatchers (até um tempo limite especificado pelo parâmetro dispatchTimeoutMs, que é de 60 segundos por padrão).

Como configurar o dispatcher Main

Em testes de unidade locais, o dispatcher Main que envolve a linha de execução de interface 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 dispatcher 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 dispatcher 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 dispatcher 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()
        }
    }
}

Caso o dispatcher Main seja substituído por um TestDispatcher, quaisquer TestDispatchers recém-criados (incluindo o StandardTestDispatcher criado por runTest) vão usar automaticamente o agendador do dispatcher Main se nenhum outro dispatcher for transmitido.

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 dispatcher 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 quando o dispatcher Main não pode 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 dispatcher 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 dispatcher 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 dispatcher 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 dispatcher 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 dispatcher (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 dispatcher Main.

Se você não estiver substituindo o dispatcher 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 dispatcher é transmitido ao runTest. Isso cria um novo StandardTestDispatcher para o TestScope usando o mesmo agendador. Também é possível transmitir o dispatcher 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 {
        // ...
    }
}

Como injetar um escopo

Se você tiver uma classe que cria corrotinas que precisa controlar durante testes, é possível injetar um escopo de corrotinas nessa classe, substituindo-a por um TestScope nos testes.

No exemplo abaixo, a classe UserState depende de um UserRepository para registrar novos usuários e buscar a lista de usuários registrados. Como essas chamadas para UserRepository estão suspendendo chamadas de função, o UserState usa o CoroutineScope injetado para iniciar uma nova corrotina dentro da função registerUser.

class UserState(
    private val userRepository: UserRepository,
    private val scope: CoroutineScope,
) {
    private val _users = MutableStateFlow(emptyList<String>())
    val users: StateFlow<List<String>> = _users.asStateFlow()

    fun registerUser(name: String) {
        scope.launch {
            userRepository.register(name)
            _users.update { userRepository.getAllUsers() }
        }
    }
}

Para testar essa classe, você pode transmitir o TestScope do runTest ao criar o objeto UserState:

class UserStateTest {
    @Test
    fun addUserTest() = runTest { // this: TestScope
        val repository = FakeUserRepository()
        val userState = UserState(repository, scope = this)

        userState.registerUser("Mona")
        advanceUntilIdle() // Let the coroutine complete and changes propagate

        assertEquals(listOf("Mona"), userState.users.value)
    }
}

Para injetar um escopo fora da função de teste, por exemplo, em um objeto em teste criado como uma propriedade na classe de teste, consulte Como criar seu próprio TestScope.

Outros recursos