← Voltar para o blog

**Testando o Tempo: Simplificando Testes Assíncronos em Go**

Resumo em português do artigo oficial do Go Blog: **Testando o Tempo: Simplificando Testes Assíncronos em Go**

O artigo do blog oficial do Go discute a importância e os desafios de testar código assíncrono, introduzindo o pacote testing/synctest, que agora está disponível para uso geral desde o Go 1.25, após um período como pacote experimental no Go 1.24. O pacote visa simplificar significativamente a escrita de testes para código concorrente e assíncrono.

O que é uma Função Assíncrona?

O artigo começa definindo a diferença entre funções síncronas e assíncronas. Uma função síncrona executa uma ação e retorna um resultado imediatamente, enquanto uma função assíncrona retorna imediatamente após ser chamada, executando a ação em segundo plano, em algum momento no futuro. Um exemplo simples é a função Cleanup que remove um diretório de cache de forma síncrona, em contraste com CleanupInBackground, que executa a remoção em uma goroutine separada.

func (c *Cache) Cleanup() {
    os.RemoveAll(c.cacheDir)
}

func (c *Cache) CleanupInBackground() {
    go os.RemoveAll(c.cacheDir)
}

Funções como context.WithDeadline também são assíncronas, pois criam um contexto que será cancelado em um momento futuro específico.

Desafios dos Testes Assíncronos

Testar funções síncronas é direto: configura-se o estado inicial, chama-se a função e verifica-se o resultado. No entanto, testar funções assíncronas é mais complexo porque é preciso esperar que a operação em segundo plano seja concluída antes de verificar o resultado. Se a espera for muito curta, o teste pode falhar porque a operação ainda não foi concluída. Se a espera for muito longa, o teste se torna lento.

Um desafio ainda maior é verificar se algo não aconteceu. Pode-se verificar que algo não aconteceu ainda, mas como ter certeza de que não acontecerá depois?

Exemplo Prático: Testando context.WithDeadline

O artigo usa a função context.WithDeadline como exemplo para ilustrar os desafios. Um teste comum para essa função é verificar se o contexto é cancelado após o prazo (deadline) especificado. Uma primeira tentativa de implementar esse teste pode ser:

func TestWithDeadlineAfterDeadline(t *testing.T) {
    deadline := time.Now().Add(1 * time.Second)
    ctx, _ := context.WithDeadline(t.Context(), deadline)

    time.Sleep(time.Until(deadline))

    if err := ctx.Err(); err != context.DeadlineExceeded {
        t.Fatalf("context not canceled after deadline")
    }
}

Este teste dorme até o deadline e então verifica se o contexto foi cancelado. O problema é que o teste pode ser flaky, ou seja, pode falhar ocasionalmente, mesmo quando o código está correto, devido a pequenas variações no tempo de execução. Para tentar corrigir isso, pode-se adicionar um tempo extra de espera:

time.Sleep(time.Until(deadline) + 100*time.Millisecond)

No entanto, isso torna o teste mais lento e ainda não garante que ele seja completamente confiável, especialmente em ambientes de CI (Continuous Integration) com alta carga.

O Dilema: Lento ou Flaky

Testes que dependem do tempo real são inerentemente lentos ou flaky. Aumentar o tempo de espera torna o teste mais lento, mas reduz a probabilidade de flakiness. Diminuir o tempo de espera torna o teste mais rápido, mas aumenta a probabilidade de flakiness.

Alternativas: Funções Síncronas e Instrumentação para Testabilidade

Uma abordagem é evitar funções assíncronas sempre que possível, favorecendo funções síncronas que podem ser executadas em goroutines separadas se necessário. Isso simplifica o teste, pois a execução da função pode ser controlada diretamente no teste.

// CleanupInBackground é difícil de testar.
cache.CleanupInBackground()

// Cleanup é fácil de testar,
// e fácil de rodar em background quando necessário.
go cache.Cleanup()

No entanto, nem sempre é possível evitar funções assíncronas. Nesses casos, uma abordagem melhor é instrumentar o código para torná-lo mais testável. Isso envolve o uso de “fake time” (tempo falso) e a capacidade de esperar que toda a atividade em segundo plano seja concluída antes de verificar o resultado.

Um exemplo de como isso pode ser implementado para o teste context.WithDeadline é:

func TestWithDeadlineAfterDeadline(t *testing.T) {
    clock := fakeClock()
    timeout := 1 * time.Second
    deadline := clock.Now().Add(timeout)

    ctx, _ := context.WithDeadlineClock(
        t.Context(), deadline, clock)

    clock.Advance(timeout)
    context.WaitUntilIdle(ctx)
    if err := ctx.Err(); err != context.DeadlineExceeded {
        t.Fatalf("context not canceled after deadline")
    }
}

Neste exemplo, fakeClock é uma implementação de um relógio falso que permite controlar o tempo no teste. context.WithDeadlineClock é uma versão modificada da função context.WithDeadline que aceita um relógio como parâmetro. clock.Advance avança o tempo no relógio falso, e context.WaitUntilIdle espera que todas as goroutines relacionadas ao contexto sejam concluídas.

Essa abordagem resolve os problemas de lentidão e flakiness, pois o tempo pode ser controlado de forma precisa e a conclusão das operações em segundo plano pode ser garantida antes de verificar o resultado.

Os dois princípios fundamentais de escrever código concorrente testável são:

  1. Usar tempo falso (se o código usa tempo).
  2. Ter alguma forma de esperar pela quiescência (quando toda a atividade em segundo plano é concluída).

O artigo reconhece que a instrumentação do código para testabilidade pode ser complexa, especialmente a parte de identificar quando toda a atividade em segundo plano foi concluída.


Artigo Original

Este e um resumo em português do artigo original publicado no blog oficial do Go.

Titulo original: Testing Time (and other asynchronicities)

Leia o artigo completo em ingles no Go Blog

Autor original: Damien Neil