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 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 oTestDispatcher
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), orunTest
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 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 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 você também pode
injetar a abordagem
CoroutineContext
o que permite ainda mais flexibilidade durante os 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 ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* 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 = ExampleRepository(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 = ExampleRepository(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
- Como testar fluxos em Kotlin no Android
- kotlinx.coroutines.test (link em inglês) no GitHub