Quando você está trabalhando em um recurso que depende do tempo ... Como você organiza o teste de unidade? Quando seus cenários de testes unitários dependem da maneira como seu programa interpreta "agora", como você os configura?
Segunda edição: após alguns dias lendo sua experiência
Percebo que as técnicas para lidar com essa situação geralmente giram em torno de um desses três princípios:
- Adicionar (código rígido) uma dependência: adicione uma pequena camada ao longo da função / objeto de tempo e sempre chame sua função de data e hora através dessa camada. Dessa forma, você pode controlar o tempo durante os casos de teste.
- Use zombarias: seu código permanece exatamente o mesmo. Nos seus testes, você substitui o objeto de tempo por um objeto de tempo falso. Às vezes, a solução envolve modificar o objeto de tempo genuíno fornecido pela sua linguagem de programação.
- Usar injeção de dependência: construa seu código para que qualquer referência de tempo seja passada como parâmetro. Então, você tem controle dos parâmetros durante os testes.
Técnicas (ou bibliotecas) específicas para um idioma são muito bem-vindas e serão aprimoradas se um trecho de código ilustrativo aparecer. Então, o mais interessante é como princípios semelhantes podem ser aplicados em qualquer plataforma. E sim ... Se eu puder aplicá-lo imediatamente no PHP, melhor do que melhor;)
Vamos começar com um exemplo simples: um aplicativo de reserva básico.
Digamos que temos a API JSON e duas mensagens: uma mensagem de solicitação e uma mensagem de confirmação. Um cenário padrão é assim:
- Você faz um pedido. Você obtém uma resposta com um token. O recurso necessário para atender a essa solicitação é bloqueado pelo sistema por 5 minutos.
- Você confirma uma solicitação, identificada pelo token. Se o token foi emitido em 5 minutos, ele será aceito (o recurso ainda estará disponível). Se tiverem passado mais de 5 minutos, é necessário fazer uma nova solicitação (o recurso foi liberado. Você precisa verificar sua disponibilidade novamente).
Aqui está o cenário de teste correspondente:
Eu faço um pedido. Confirmo (imediatamente) com o token que recebi. Minha confirmação é aceita.
Eu faço um pedido. Eu espero 3 minutos. Confirmo com o token que recebi. Minha confirmação é aceita.
Eu faço um pedido. Eu espero 6 minutos. Confirmo com o token que recebi. Minha confirmação foi rejeitada.
Como podemos programar esses testes de unidade? Que arquitetura devemos usar para que essas funcionalidades permaneçam testáveis?
Editar Nota: Quando disparamos uma solicitação, o tempo é armazenado em um banco de dados em um formato que perde qualquer informação sobre milissegundos.
EXTRA - mas talvez um pouco detalhado: Aqui estão os detalhes sobre o que eu descobri fazendo meu "dever de casa":
Criei meu recurso com uma dependência de uma função de tempo própria. Minha função VirtualDateTime possui um método estático get_time () que eu chamo onde costumava chamar new DateTime (). Essa função de tempo permite simular e controlar que horas são "agora", para que eu possa criar testes como: "Defina agora para 21 de janeiro de 2014 às 16h15. Faça uma solicitação. Siga em frente 3 minutos. Faça a confirmação.". Isso funciona bem, à custa da dependência, e "não é um código tão bonito".
Uma solução um pouco mais integrada seria criar minha própria função myDateTime que estenda o DateTime com funções adicionais de "tempo virtual" (o mais importante, defina agora o que eu quero). Isso está tornando o código da primeira solução um pouco mais elegante (uso do novo myDateTime em vez do novo DateTime), mas acaba sendo muito semelhante: eu tenho que criar meu recurso usando minha própria classe, criando assim uma dependência.
Já pensei em invadir a função DateTime, para fazê-la funcionar com minha dependência quando eu precisar. Existe alguma maneira simples e clara de substituir uma classe por outra? (Acho que recebi uma resposta para isso: veja espaço para nome abaixo).
Em um ambiente PHP, eu li "Runkit" pode me permitir fazer isso (hackear a função DateTime) dinamicamente. O que é legal é que eu seria capaz de verificar se estou executando no ambiente de teste antes de modificar qualquer coisa sobre o DateTime e deixá-lo intocado na produção [1] . Isso parece muito mais seguro e limpo do que qualquer hack manual do DateTime.
Injeção de dependência de uma função de relógio em cada classe que usa o tempo [2] . Isso não é um exagero? Vejo que em alguns ambientes isso se torna muito útil [5] . Nesse caso, eu não gosto muito disso.
Remova a dependência do tempo em todas as funções [2] . Isso é sempre viável? (Veja mais exemplos abaixo)
Usando namespaces [2] [3] ? Isso parece muito bom. Eu poderia zombar de DateTime com minha própria função DateTime que estende \ DateTime ... Use DateTime :: setNow ("2014-01-21 16:15:00") e DateTime :: wait ("+ 3 minutos"). Isso cobre praticamente o que eu preciso. E se usarmos a função time ()? Ou outras funções de tempo? Eu ainda tenho que evitar o uso deles no meu código original. Ou eu precisaria garantir que qualquer função de hora do PHP que eu uso no meu código seja substituída nos meus testes ... Existe alguma biblioteca disponível que faça exatamente isso?
Eu tenho procurado uma maneira de alterar a hora do sistema apenas para um thread. Parece que não há [4] . É uma pena: uma função PHP "simples" para "Definir hora para 21 de janeiro de 2014 às 16h15 para este encadeamento" seria um ótimo recurso para esse tipo de teste.
Altere a data e hora do sistema para o teste, usando exec (). Isso pode funcionar se você não tiver medo de mexer com outras coisas no servidor. E você precisa adiar o tempo do sistema depois de executar o teste. Pode fazer o truque em algumas situações, mas parece bastante "hacky".
Isso parece um problema muito padrão para mim. No entanto, ainda sinto falta de uma maneira simples e genérica de lidar com isso. Talvez eu tenha perdido alguma coisa? Por favor, compartilhe sua experiência!
NOTA: Aqui estão algumas outras situações em que podemos ter necessidades de teste semelhantes.
Qualquer recurso que funcione com algum tipo de tempo limite (por exemplo, jogo de xadrez?)
processando uma fila que aciona eventos em um determinado momento (no cenário acima, podemos continuar assim: um dia antes do início da reserva, desejo enviar um email ao usuário com todos os detalhes. Cenário de teste ...)
Você deseja configurar um ambiente de teste com dados coletados no passado - você gostaria de vê-lo como se fosse agora. [1]
1 /programming/3271735/simulate-different-server-datetimes-in-php
2 /programming/4221480/how-to-change-current-time-for-unit-testing-date-functions-in-php
3 http://www.schmengler-se.de/en/2011/03/php-mocking-built-in-functions-like-time-in-unit-tests/
DateTime now
para o código em vez de um relógio.