Como testar com unit tests código que depende do tempo

Testar código que depende da hora atual levanta questões de design mais profundas sobre como o software deve interagir com estado externo. Comentadores comparam abordagens como clocks injetados por dependência, passagem de timestamps para funções puras, monkey-patching de APIs de tempo e shims em nível de sistema como libfaketime, observando que as escolhas variam por linguagem e por se você está escrevendo testes unitários ou de integração. Muitos argumentam que tratar o tempo como uma dependência de primeira classe melhora a testabilidade e a arquitetura, enquanto outros apontam limites difíceis quando bibliotecas de terceiros, características de desempenho ou temporização de hardware real estão envolvidos.

Princípio central: tratar o tempo como uma dependência

  • Muitos argumentam que “o tempo é uma dependência como qualquer outra.”
  • Padrões recomendados: injetar uma interface de clock/timer, passar uma função now(), ou passar timestamps explícitos para a lógica.
  • Ordem de preferência (para alguns):
    • Melhor: a lógica recebe dados de tempo como argumento.
    • Em seguida: a lógica recebe uma função que produz tempo.
    • Depois: injetar um objeto de clock/timer via DI.

Ferramentas específicas de linguagem e framework

  • O artigo em C++ foi visto como restrito; outras linguagens oferecem mecanismos mais fáceis.
  • Linguagens dinâmicas (JS, Python, Ruby) frequentemente fazem monkey-patch do tempo ou usam bibliotecas: Jest fake timers, freezegun/time-machine (Python), Timecop/Rails travel_to (Ruby).
  • Java normalmente injeta Clock. .NET 8 adiciona TimeProvider e FakeTimeProvider. Código Go frequentemente passa time.Now como parâmetro de função.

Bancos de dados e múltiplos relógios

  • Usar NOW() em SQL não é inerentemente ruim, mas pode introduzir um segundo relógio.
  • Problemas surgem quando o tempo da aplicação é mockado, mas o tempo do banco é real, ou quando o agendador e o executor usam máquinas/relógios diferentes.
  • Muitos sugerem passar o tempo para as queries para manter consistência.

Bibliotecas de terceiros e escopo de teste

  • Se código que você não controla chama o tempo real, talvez seja impossível injetar seu próprio clock.
  • Respostas sugeridas:
    • Usar testes de integração e aceitar verificações mais lentas/menos isoladas.
    • Envolver a biblioteca por trás da sua própria interface e fornecer uma implementação de teste.
    • Debate sobre se testes de primeira parte deveriam alguma vez testar explicitamente o comportamento de terceiros; há forte discordância aqui.

Intercepção de tempo em nível de sistema

  • Ferramentas como libfaketime (e bibliotecas experimentais semelhantes) interceptam chamadas de sistema ou funções vDSO para falsificar o tempo no nível do processo.
  • São vistas como poderosas, mas às vezes desajeitadas (variáveis de ambiente, LD_PRELOAD) e exageradas para testes unitários simples.

Testes de integração que dependem do tempo

  • Testes ingênuos baseados em sleep() são lentos e instáveis sob carga.
  • Solução comum: fazer polling de efeitos colaterais observáveis em vez de esperar durações fixas.
  • Outras propostas: faketime no nível do processo, clocks locais por tenant controláveis pelo harness de teste, ou “test clocks” injetados em um nível arquitetural mais alto.

Mocks, complexidade de DI e crítica ao artigo

  • Alguns se preocupam que injetar clocks e mocks adiciona dependências e torna o código mais difícil de trabalhar; outros dizem que clocks injetados ajudam muito tanto nos testes quanto no depuramento.
  • Mocks são vistos como aceitáveis se testam interfaces públicas e bem definidas, e se não forem usados em excesso.
  • Alguns criticam o artigo por ser simplista demais: sistemas reais sensíveis ao tempo (desempenho, hardware, multimídia) têm desafios que vão muito além de fábricas de clock e templates.