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/
fonte
DateTime now
para o código em vez de um relógio.Respostas:
Vou usar o pseudocódigo semelhante ao Python, baseado nos seus casos de teste, para (espero) ajudar a explicar um pouco essa resposta, em vez de apenas expor. Mas, resumindo: esta resposta é basicamente apenas um exemplo de injeção de dependência de nível de função única / inversão de dependência , em vez de aplicar o princípio a uma classe / objeto inteiro.
No momento, parece que seus testes estão estruturados da seguinte maneira:
Com implementações, algo como:
Em vez disso, tente esquecer o "agora" e informe a ambas as funções que horas usar. Se a maior parte do tempo for "agora", você pode fazer uma das duas coisas principais para manter o código de chamada simples:
None
(null
em PHP) e defina-o como "agora" se nenhum valor for fornecido.Por exemplo, as duas funções acima reescritas na segunda versão podem ser assim:
Dessa forma, partes do código existentes não precisam ser modificadas para passar "agora", enquanto as partes que você realmente deseja testar podem ser testadas com muito mais facilidade:
Ele também cria flexibilidade adicional no futuro, portanto, menos mudanças serão necessárias se as partes importantes precisarem ser reutilizadas. Para dois exemplos que vêm à mente: Uma fila em que as solicitações talvez precisem ser criadas momentos tarde demais, mas ainda usem a hora correta, ou as confirmações precisam acontecer com as solicitações anteriores, como parte das verificações de integridade dos dados.
Tecnicamente, pensar no futuro dessa maneira pode violar o YAGNI , mas na verdade não estamos construindo para esses casos teóricos - essa mudança seria apenas para testes mais fáceis, que apenas suportam esses casos teóricos.
Pessoalmente, tento evitar qualquer tipo de zombaria, tanto quanto possível, e prefiro funções puras como acima. Dessa forma, o código sendo testado é idêntico ao código sendo executado fora dos testes, em vez de substituir partes importantes por zombarias.
Tivemos uma ou duas regressões que ninguém notou por um bom mês porque essa parte específica do código não era frequentemente testada no desenvolvimento (não estávamos trabalhando ativamente nele), foi lançada um pouco quebrada (portanto, não houve nenhum erro relatórios) e as zombarias usadas escondiam um bug na interação entre as funções auxiliares. Algumas pequenas modificações para que as simulações não fossem necessárias e muito mais poderia ser facilmente testado, incluindo a correção dos testes em torno dessa regressão.
Esse é o que deve ser evitado a todo custo para testes de unidade. Não há realmente nenhuma maneira de saber que tipos de inconsistências ou condições de corrida podem ser introduzidas para causar falhas intermitentes (para aplicativos não relacionados ou colegas de trabalho na mesma caixa de desenvolvimento), e um teste com falha / erro pode não atrasar o relógio corretamente.
fonte
Pelo meu dinheiro, manter uma dependência em alguma classe de relógio é a maneira mais clara de lidar com testes dependentes do tempo. No PHP, pode ser tão simples quanto uma classe com uma única função
now
que retorna a hora atual usandotime()
ounew DateTime()
.A injeção dessa dependência permite que você manipule o tempo em seus testes e use o tempo real na produção, sem precisar escrever muito código extra.
fonte
A primeira coisa que você precisa fazer é remover os números mágicos do seu código. Nesse caso, seu número mágico "5 minutos". Você não apenas precisará inevitavelmente alterá-las mais tarde, quando algum cliente exigir, como também torna os testes de unidade muito melhores. Quem vai esperar 5 minutos para que o teste seja executado?
Segundo, se você ainda não o fez, use o UTC para os registros de data e hora reais. Isso evitará problemas com o horário de verão e a maioria dos outros soluços do relógio.
No teste de unidade, mude a duração configurada para algo como 500ms e faça seu teste normal: funciona de uma maneira antes do tempo limite e outra depois.
~ 1s ainda é bastante lento para testes de unidade, e você provavelmente precisará de algum tipo de limite para levar em consideração o desempenho da máquina e as mudanças de relógio devido ao NTP (ou outro desvio). Mas, na minha experiência, esta é a melhor maneira prática de testar o tempo de teste unitário na maioria dos sistemas. Simples, rápido, confiável. A maioria das outras abordagens (como você descobriu) exige muito trabalho e / ou é muito propensa a erros.
fonte
Quando eu aprendi TDD, eu estava trabalhando em um ambiente c # . A maneira como aprendi a fazer isso é ter um ambiente de IoC completo, que também inclui um
ITimeService
responsável por todos os serviços de tempo e que pode ser facilmente ridicularizado nos testes.Atualmente estou trabalhando em rubi , e o tempo de stub é muito mais fácil - você simplesmente stub time :
fonte
Use o Microsoft Fakes - em vez de alterar seu código para se adequar à ferramenta, a ferramenta altera seu código em tempo de execução, injetando um novo objeto Time (que você define em seu teste) no lugar do relógio padrão.
Se você estiver executando uma linguagem dinâmica - o Runkit é exatamente o mesmo tipo de coisa.
Por exemplo, com Fakes:
Portanto, quando o método de texto chama,
return DateTime.Now
ele retornará o tempo que você injetou, e não o tempo atual real.fonte
Eu explorei a opção namespaces em PHP. Os espaços para nome nos dão a oportunidade de adicionar algum comportamento de teste à função DateTime. Assim, nossos objetos originais permanecem intocados.
Primeiro, configuramos um objeto DateTime falso em nosso espaço para nome, para que possamos, em nossos testes, gerenciar o horário considerado "agora".
Em nossos testes, podemos gerenciar esse tempo por meio das funções estáticas DateTime :: set_now ($ my_time) e DateTime :: modify_now ("add some time").
Primeiro vem um objeto de reserva falso . Este objeto é o que queremos testar - observe o uso do novo DateTime () sem parâmetros em dois lugares. Uma breve visão geral:
Nosso objeto DateTime substituindo o objeto DateTime principal se parece com isso . Todas as chamadas de função permanecem inalteradas. Nós apenas "enganchamos" o construtor para simular nosso "agora é sempre que eu escolho" o comportamento. Mais importante,
Nossos testes são então muito diretos. É assim com a unidade php (versão completa aqui ):
Essa estrutura pode ser facilmente reproduzida onde quer que eu precise gerenciar o "agora" manualmente: só preciso incluir o novo objeto DateTime e usar seus métodos estáticos nos meus testes.
fonte
new Datetime
um método separado para ser zombeteiro.