Recentemente, eu estava escrevendo um pequeno pedaço de código que indicava de maneira amigável ao ser humano quantos anos um evento tem. Por exemplo, isso pode indicar que o evento aconteceu "Três semanas atrás" ou "Um mês atrás" ou "Ontem".
Os requisitos eram relativamente claros e esse era um caso perfeito para o desenvolvimento orientado a testes. Eu escrevi os testes um por um, implementando o código para passar em cada teste, e tudo parecia funcionar perfeitamente. Até que um bug apareceu na produção.
Aqui está o trecho de código relevante:
now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
return "Today"
yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
return "Yesterday"
delta = (now - event_date).days
if delta < 7:
return _number_to_text(delta) + " days ago"
if delta < 30:
weeks = math.floor(delta / 7)
if weeks == 1:
return "A week ago"
return _number_to_text(weeks) + " weeks ago"
if delta < 365:
... # Handle months and years in similar manner.
Os testes estavam verificando o caso de um evento acontecendo hoje, ontem, quatro dias atrás, duas semanas atrás, uma semana atrás, etc., e o código foi construído de acordo.
O que eu perdi é que um evento pode acontecer um dia antes de ontem, sendo um dia atrás: por exemplo, um evento ocorrendo vinte e seis horas atrás seria um dia atrás, e não exatamente ontem, se agora for 1h. Mais exatamente, é um ponto alguma coisa, mas como o delta
é um número inteiro, será apenas um. Nesse caso, o aplicativo exibe "Um dia atrás", o que é obviamente inesperado e não tratado no código. Pode ser corrigido adicionando:
if delta == 1:
return "A day ago"
logo após calcular o delta
.
Embora a única consequência negativa do bug seja que eu perdi meia hora me perguntando como esse caso poderia acontecer (e acreditando que isso tenha a ver com fusos horários, apesar do uso uniforme do UTC no código), sua presença está me incomodando. Indica que:
- É muito fácil cometer um erro lógico, mesmo em um código-fonte tão simples.
- O desenvolvimento orientado a testes não ajudou.
Também preocupante é que não consigo ver como esses erros podem ser evitados. Além de pensar mais antes de escrever código, a única maneira de pensar é adicionar muitas afirmações para os casos que acredito que nunca aconteceriam (como eu acreditava que um dia atrás é necessariamente ontem) e, em seguida, percorrer cada segundo para nos últimos dez anos, verificando qualquer violação de afirmação, que pareça muito complexa.
Como eu poderia evitar criar esse bug em primeiro lugar?
fonte
Respostas:
Esses são os tipos de erros que você normalmente encontra na etapa de refatoração de vermelho / verde / refatorador. Não esqueça esse passo! Considere um refator como o seguinte (não testado):
Aqui você criou três funções em um nível mais baixo de abstração, que são muito mais coesas e mais fáceis de testar isoladamente. Se você deixasse de fora um intervalo de tempo pretendido, ele se destacaria como um polegar dolorido nas funções auxiliares mais simples. Além disso, ao remover a duplicação, você reduz o potencial de erro. Você realmente teria que adicionar código para implementar seu caso quebrado.
Outros casos de teste mais sutis também vêm à mente mais rapidamente quando se olha para uma forma refatorada como esta. Por exemplo, o que deve ser
best_unit
feito sedelta
for negativo?Em outras palavras, a refatoração não é apenas para torná-la bonita. Isso torna mais fácil para os humanos detectar erros que o compilador não consegue.
fonte
pluralize
somente trabalhar para um subconjunto de palavras em inglês será uma responsabilidade.pluralize
usandonum
eunit
criar uma chave de algum tipo para extrair uma string de formato de algum arquivo de tabela / recurso. OU você pode precisar de uma reescrita completa da lógica, porque você precisa de unidades diferentes ;-)Parece que ajudou, mas você não fez um teste para o cenário "um dia atrás". Presumivelmente, você adicionou um teste depois que este caso foi encontrado; isso ainda é TDD, quando erros são encontrados, você escreve um teste de unidade para detectar o erro e, em seguida, corrige-o.
Se você esquecer de escrever um teste para um comportamento, o TDD não terá nada para ajudá-lo; você esquece de escrever o teste e, portanto, não escreve a implementação.
fonte
datetime.utcnow()
da função e, em vez disso, passarnow
como um argumento (reproduzível).Os testes não ajudarão muito se um problema estiver mal definido. Evidentemente, você está misturando dias do calendário com dias contados em horas. Se você permanecer nos dias corridos, às 01:00, 26 horas atrás, não será ontem. E se você ficar com horas, 26 horas atrás será arredondado para um dia atrás, independentemente da hora.
fonte
Você não pode. O TDD é ótimo em protegê-lo de possíveis problemas que você conhece. Não ajuda se você tiver problemas que nunca considerou. Sua melhor aposta é ter alguém testando o sistema; eles podem encontrar os casos extremos que você nunca considerou.
Leitura relacionada: É possível alcançar o estado de erro zero absoluto para software em grande escala?
fonte
Normalmente, existem duas abordagens que considero que podem ajudar.
Primeiro, procuro os casos extremos. Estes são os locais onde o comportamento muda. No seu caso, o comportamento muda em vários pontos ao longo da sequência de dias inteiros positivos. Há um caso de borda em zero, em um, em sete, etc. Então eu escreveria casos de teste nos casos de borda e em torno deles. Eu teria casos de teste em -1 dias, 0 dias, 1 hora, 23 horas, 24 horas, 25 horas, 6 dias, 7 dias, 8 dias, etc.
A segunda coisa que eu procuraria é padrões de comportamento. Na sua lógica por semanas, você tem um tratamento especial por uma semana. Você provavelmente tem uma lógica semelhante em cada um dos seus outros intervalos não mostrados. Essa lógica não está presente há dias, no entanto. Eu olhava para isso com desconfiança até que eu pudesse explicar de forma verificável por que esse caso é diferente ou acrescentar a lógica.
fonte
Você não pode detectar erros lógicos presentes nos seus requisitos com o TDD. Mas ainda assim, o TDD ajuda. Você encontrou o erro, afinal, e adicionou um caso de teste. Mas, fundamentalmente, o TDD apenas garante que o código esteja em conformidade com o seu modelo mental. Se o seu modelo mental é defeituoso, os casos de teste não os capturam.
Mas lembre-se de que, enquanto corrigia o bug, os casos de teste que você já tinha garantido que nenhum comportamento funcional existente fosse quebrado. Isso é muito importante, é fácil corrigir um erro, mas introduzir outro.
Para encontrar esses erros antecipadamente, você geralmente tenta usar casos de teste baseados em classe de equivalência. usando esse princípio, você escolheria um caso de cada classe de equivalência e, em seguida, todos os casos extremos.
Você escolheria uma data de hoje, ontem, alguns dias atrás, exatamente uma semana atrás e várias semanas atrás, como exemplos de cada classe de equivalência. Ao testar datas, você também deve garantir que seus testes não usem a data do sistema, mas usem uma data predeterminada para comparação. Isso também destacaria alguns casos extremos: você executaria seus testes em algum horário arbitrário do dia, executando-o diretamente após a meia-noite, diretamente antes da meia-noite e até diretamente à meia - noite. Isso significa que para cada teste, haveria quatro tempos de base com os quais é testado.
Então você adicionaria sistematicamente casos extremos a todas as outras classes. Você tem o teste para hoje. Portanto, adicione um tempo antes e depois do comportamento mudar. O mesmo para ontem. O mesmo para uma semana atrás etc.
As chances são de que, ao enumerar todos os casos extremos de maneira sistemática e anotando casos de teste para eles, você descobre que sua especificação está faltando alguns detalhes e os adiciona. Observe que lidar com datas é algo que as pessoas geralmente erram, porque as pessoas esquecem de escrever seus testes para que possam ser executadas em horários diferentes.
Note, no entanto, que a maior parte do que escrevi tem pouco a ver com TDD. É sobre escrever classes de equivalência e garantir que suas próprias especificações sejam detalhadas o suficiente sobre elas. Esse é o processo com o qual você minimiza erros lógicos. O TDD apenas garante que seu código esteja em conformidade com o seu modelo mental.
É difícil criar casos de teste . O teste baseado em classe de equivalência não é o fim de tudo e, em alguns casos, pode aumentar significativamente o número de casos de teste. No mundo real, adicionar todos esses testes geralmente não é economicamente viável (embora, em teoria, deva ser feito).
fonte
Por que não? Parece uma boa ideia!
Adicionar contratos (asserções) ao código é uma maneira bastante sólida de melhorar sua correção. Geralmente, nós os adicionamos como pré - condições na entrada da função e pós - condições no retorno da função. Por exemplo, podemos adicionar uma pós-condição de que todos os valores retornados têm o formato "A [unidade] atrás" ou "[número] [unidade] s atrás". Quando feito de maneira disciplinada, isso leva ao design por contrato e é uma das maneiras mais comuns de escrever código de alta garantia.
Criticamente, os contratos não devem ser testados; elas são tantas especificações do seu código quanto seus testes. No entanto, você pode testar através dos contratos: chame o código em seu teste e, se nenhum dos contratos apresentar erros, o teste será aprovado. Fazer um loop a cada segundo dos últimos dez anos é um pouco demais. Mas podemos aproveitar outro estilo de teste chamado teste baseado em propriedades .
No PBT, em vez de testar saídas específicas do código, você testa se a saída obedece a alguma propriedade. Por exemplo, uma propriedade de uma
reverse()
função é que para qualquer listal
,reverse(reverse(l)) = l
. A vantagem de escrever testes como esse é que você pode fazer com que o mecanismo PBT gere algumas centenas de listas arbitrárias (e algumas patológicas) e verifique se todas elas possuem essa propriedade. Caso contrário , o mecanismo "encolhe" o caso com falha para encontrar uma lista mínima que quebra seu código. Parece que você está escrevendo Python, que tem Hipótese como o principal framework PBT.Portanto, se você deseja uma boa maneira de encontrar casos extremos complicados em que talvez não pense, usar contratos e testes baseados em propriedades juntos ajudará muito. Isso não substitui a gravação de testes de unidade, é claro, mas aumenta, o que é realmente o melhor que podemos fazer como engenheiros.
fonte
/(today)|(yesterday)|([2-6] days ago)|...
) e, em seguida, pode executar o processo com entradas selecionadas aleatoriamente até encontrar uma que não esteja no conjunto de saídas esperadas. A adoção dessa abordagem teria detectado esse bug e não exigiria a percepção de que o bug poderia existir de antemão.Este é um exemplo em que adicionar um pouco de modularidade teria sido útil. Se um segmento de código propenso a erros for usado várias vezes, é recomendável agrupá-lo em uma função, se possível.
fonte
O TDD funciona melhor como uma técnica se a pessoa que está escrevendo os testes for contraditória. Isso é difícil se você não estiver em programação pareada, então outra maneira de pensar sobre isso é:
Essa é uma arte diferente, que se aplica à escrita de código correto com ou sem TDD, e talvez tão complexa (se não mais) do que realmente escrever código. É algo que você precisa praticar, e é algo para o qual não existe uma resposta única, fácil e simples.
A técnica principal para escrever software robusto também é a técnica principal para entender como escrever testes eficazes:
Entenda as pré-condições para uma função - os estados válidos (ou seja, de quais suposições você está fazendo sobre o estado da classe em que a função é um método) e os intervalos válidos de parâmetros de entrada - cada tipo de dado tem um intervalo de valores possíveis - um subconjunto dos quais será tratado por sua função.
Se você simplesmente não fizer nada além de testar explicitamente essas suposições na entrada de funções e garantir que uma violação seja registrada ou lançada e / ou que os erros de função sejam eliminados sem tratamento adicional, você poderá saber rapidamente se o seu software está falhando na produção, torne-o robusto tolerante a erros e desenvolva suas habilidades de redação de testes contraditórios.
NB. Existe toda uma literatura sobre condições pré e pós, invariantes e assim por diante, além de bibliotecas que podem aplicá-las usando atributos. Pessoalmente, não sou fã de ser tão formal, mas vale a pena investigar.
fonte
Este é um dos fatos mais importantes sobre o desenvolvimento de software: é absolutamente impossível escrever código sem erros.
O TDD não impedirá que você introduza bugs correspondentes a casos de teste nos quais você não pensou. Ele também não impedirá que você escreva um teste incorreto sem realizá-lo; depois, escreva o código incorreto que passa no teste de buggy. E todas as outras técnicas de desenvolvimento de software já criadas têm falhas semelhantes. Como desenvolvedores, somos humanos imperfeitos. No final do dia, não há como escrever um código 100% livre de erros. Isso nunca aconteceu e nunca acontecerá.
Isso não quer dizer que você deva desistir da esperança. Embora seja impossível escrever um código completamente perfeito, é muito possível escrever um código que tenha tão poucos erros que apareçam em casos tão raros que o software seja extremamente prático. Software que não apresenta comportamento de buggy na prática é muito possível de escrever.
Mas, para escrevê-lo, precisamos aceitar o fato de que produziremos software de buggy. Quase todas as práticas modernas de desenvolvimento de software são, em algum nível, criadas para impedir que bugs apareçam em primeiro lugar ou para nos protegermos das conseqüências dos bugs que inevitavelmente produzimos:
A solução definitiva para o problema que você identificou não é combater o fato de que você não pode garantir que escreverá um código sem erros, mas sim adotá-lo. Adote as melhores práticas do setor em todas as áreas do seu processo de desenvolvimento, e você fornecerá constantemente código aos seus usuários que, embora não sejam perfeitamente perfeitos, são mais do que robustos o suficiente para o trabalho.
fonte
Você simplesmente não tinha pensado neste caso antes e, portanto, não tinha um caso de teste para ele.
Isso acontece o tempo todo e é apenas normal. É sempre uma compensação quanto esforço você coloca na criação de todos os casos de teste possíveis. Você pode gastar um tempo infinito para considerar todos os casos de teste.
Para um piloto automático de aeronave, você gastaria muito mais tempo do que para uma ferramenta simples.
Muitas vezes, ajuda a pensar sobre os intervalos válidos de suas variáveis de entrada e testar esses limites.
Além disso, se o testador for uma pessoa diferente do desenvolvedor, geralmente são encontrados casos mais significativos.
fonte
Esse é outro erro lógico no seu código para o qual você ainda não tem um teste de unidade :) - seu método retornará resultados incorretos para usuários em fusos horários não UTC. Você precisa converter "agora" e a data do evento para o fuso horário local do usuário antes de calcular.
Exemplo: na Austrália, um evento acontece às 9h, horário local. Às 11h, será exibido como "ontem" porque a data UTC foi alterada.
fonte
Deixe alguém escrever os testes. Dessa forma, alguém não familiarizado com sua implementação pode verificar situações raras nas quais você não pensou.
Se possível, injete casos de teste como coleções. Isso torna a adição de outro teste tão fácil quanto a adição de outra linha
yield return new TestCase(...)
. Isso pode ir na direção do teste exploratório , automatizando a criação de casos de teste: "Vamos ver o que o código retorna por todos os segundos de uma semana atrás".fonte
Você parece ter a idéia errada de que, se todos os seus testes forem aprovados, você não terá erros. Na realidade, se todos os seus testes forem aprovados, todo o comportamento conhecido estará correto. Você ainda não sabe se o comportamento desconhecido está correto ou não.
Felizmente, você está usando cobertura de código com seu TDD. Adicione um novo teste para o comportamento inesperado. Em seguida, você pode executar apenas o teste do comportamento inesperado para ver qual caminho ele realmente percorre no código. Depois de conhecer o comportamento atual, você pode fazer uma alteração para corrigi-lo e, quando todos os testes passarem novamente, você saberá que o fez corretamente.
Isso ainda não significa que seu código está livre de erros, apenas que é melhor do que antes e, mais uma vez, todo o comportamento conhecido está correto!
O uso correto do TDD não significa que você escreverá um código sem erros, mas sim menos erros. Você diz:
Isso significa que o comportamento mais de um dia, mas não ontem foi especificado nos requisitos? Se você perdeu um requisito por escrito, a culpa é sua. Se você percebeu que os requisitos estavam incompletos ao codificá-lo, é bom para você! Se todo mundo que trabalhou nos requisitos perdeu esse caso, você não é pior que os outros. Todo mundo comete erros, e quanto mais sutis são, mais fáceis são de perder. A grande vantagem aqui é que o TDD não impede todos os erros!
fonte
Sim. O desenvolvimento orientado a testes não muda isso. Você ainda pode criar bugs no código real e também no código de teste.
Ah, mas aconteceu! Primeiro de tudo, quando você notou o bug, você já tinha a estrutura de teste completa em funcionamento e apenas tinha que corrigi-lo no teste (e o código real). Em segundo lugar, você não sabe quantos bugs você teria se não tivesse feito o TDD no começo.
Você não pode. Nem a NASA encontrou uma maneira de evitar bugs; nós, seres humanos inferiores, certamente também não.
Isso é uma falácia. Um dos maiores benefícios do TDD é que você pode codificar com menos raciocínio, porque todos esses testes ao menos capturam regressões muito bem. Além disso, mesmo, ou especialmente com TDD, é que não esperado para entregar código livre de bugs, em primeiro lugar (ou sua velocidade de desenvolvimento irá simplesmente ficar paralisada).
Isso claramente entraria em conflito com o princípio de apenas codificar o que você realmente precisa no momento. Você pensou que precisava desses casos, e assim foi. Era um pedaço de código não crítico; como você disse, não houve danos, exceto por 30 minutos.
Para código de missão crítica, você realmente pode fazer o que disse, mas não para o código padrão diário.
Você não Você confia em seus testes para encontrar a maioria das regressões; você mantém o ciclo de refatoração de vermelho-verde, escrevendo testes antes / durante a codificação real e (importante!) implementa a quantidade mínima necessária para fazer a troca de vermelho-verde (nem mais, nem menos). Isso resultará em uma ótima cobertura de teste, pelo menos positiva.
Quando, se não, você encontra um bug, escreve um teste para reproduzi-lo e corrige o bug com o mínimo de trabalho possível para fazer com que o teste passe de vermelho para verde.
fonte
Você acabou de descobrir que, por mais que tente, nunca conseguirá capturar todos os erros possíveis no seu código.
Então, o que isso significa é que, mesmo tentando capturar todos os bugs, é um exercício de futilidade; portanto, você só deve usar técnicas como TDD como forma de escrever um código melhor, código que tenha menos bugs, e não 0 bugs.
Isso, por sua vez, significa que você deve gastar menos tempo usando essas técnicas e gastar esse tempo economizado trabalhando em maneiras alternativas de encontrar os bugs que passam pela rede de desenvolvimento.
alternativas como teste de integração ou uma equipe de teste, teste do sistema e criação de log e análise desses logs.
Se você não conseguir capturar todos os bugs, precisará de uma estratégia para mitigar os efeitos dos bugs que passam por você. Se você tiver que fazer isso de qualquer maneira, colocar mais esforço nisso faz mais sentido do que tentar (em vão) detê-los em primeiro lugar.
Afinal, é inútil gastar uma fortuna gastando tempo escrevendo testes e, no primeiro dia em que você entrega seu produto a um cliente, ele cai, principalmente se você não tem idéia de como encontrar e resolver esse bug. A resolução de erros post mortem e pós-entrega é tão importante e precisa de mais atenção do que a maioria das pessoas gasta em testes de unidade de escrita. Salve o teste de unidade para os bits complicados e não tente a perfeição com antecedência.
fonte
That in turn means you should spend less time using these techniques
- mas você acabou de dizer que ajudará com menos erros ?!