Como evitar erros lógicos no código, quando o TDD não ajudou?

67

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?

Arseni Mourzenko
fonte
38
Por ter um caso de teste para isso? Parece que foi assim que você descobriu depois, e se mescla com o TDD.
Οurous
63
Você acabou de perceber por que não sou fã de desenvolvimento orientado a testes - na minha experiência, a maioria dos erros detectados na produção são cenários em que ninguém pensou. O desenvolvimento orientado a testes e os testes de unidade não fazem nada por isso. (Testes de unidade têm valor na detecção de erros introduzidos através de edições futuras, no entanto.)
Loren Pechtel
102
Repita comigo: "Não há balas de prata, incluindo TDD". Não há processo, nem conjunto de regras, nem algoritmo que você possa seguir roboticamente para produzir um código perfeito. Se houvesse, poderíamos automatizar todo o processo e terminar com ele.
jpmc26
43
Parabéns, você redescobriu a antiga sabedoria de que nenhum teste pode provar a ausência de bugs. Mas se você estiver procurando técnicas para criar uma melhor cobertura do possível domínio de entrada, precisará fazer uma análise completa do domínio, dos casos extremos e das classes de equivalência desse domínio. Todas as técnicas antigas e bem conhecidas, muito conhecidas antes da invenção do termo TDD.
Doc Brown
80
Não estou tentando ser irritante, mas sua pergunta pode aparentemente ser reformulada como "como penso em coisas em que não pensei?". Não tenho certeza do que isso tem a ver com TDD.
Jared Smith

Respostas:

57

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):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

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_unitfeito se deltafor 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.

Karl Bielefeldt
fonte
12
O próximo passo é internacionalizar, e pluralizesomente trabalhar para um subconjunto de palavras em inglês será uma responsabilidade.
Deduplicator
@ Deduplicator, com certeza, mas, dependendo de quais idiomas / culturas você segmentar, você poderá modificar apenas pluralizeusando nume unitcriar 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 ;-)
Hulk
4
Um problema permanece mesmo com essa refatoração, que é que "ontem" não faz muito sentido nas primeiras horas da manhã (logo após as 12h01). Em termos humanos, algo que aconteceu às 23h59 não muda repentinamente de "hoje" para "ontem" quando o relógio passa da meia-noite. Em vez disso, muda de "1 minuto atrás" para "2 minutos atrás". "Hoje" é muito grosseiro em termos de algo que aconteceu há alguns minutos atrás, e "ontem" está cheio de problemas para as corujas noturnas.
David Hammen
@DavidHammen Esse é um problema de usabilidade e depende da precisão que você precisa ser. Quando você quer saber pelo menos até a hora, eu não pensaria que "ontem" seja bom. "24 horas atrás" é muito mais claro e é uma expressão humana comumente usada para enfatizar o número de horas. Os computadores que estão tentando ser "amigáveis ​​ao ser humano" quase sempre entendem errado e o generalizam demais para "ontem", o que é muito vago. Mas, para saber isso, você precisará entrevistar os usuários para ver o que eles pensam. Para algumas coisas, você realmente deseja a data e a hora exata, para que "ontem" esteja sempre errado.
Brandin
149

O desenvolvimento orientado a testes não ajudou.

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.

esoterik
fonte
2
Qualquer coisa poderia ser feita, se o desenvolvedor não tivesse usado o tdd, teria muito mais chances de perder outros casos.
Caleb
75
E, além disso, pense em quanto tempo foi economizado quando consertamos o bug? Ao ter os testes existentes, eles souberam instantaneamente que sua mudança não quebrou o comportamento existente. E eles estavam livres para adicionar os novos casos de teste e refatorar sem ter que executar extensos testes manuais posteriormente.
Caleb
15
TDD é tão bom quanto os testes escritos.
Mindwin
Outra observação: adicionar o teste para este caso melhorará o design, forçando-nos a tirar isso datetime.utcnow()da função e, em vez disso, passar nowcomo um argumento (reproduzível).
precisa
114

um evento acontecendo vinte e seis horas atrás seria um dia atrás

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.

Kevin Krumwiede
fonte
45
Este é um ótimo ponto a se fazer. A falta de um requisito não significa necessariamente que seu processo de implementação falhou. Significa apenas que o requisito não foi bem definido. (Ou você simplesmente cometeu um erro humano, o que vai acontecer ao longo do tempo)
Caleb
Esta é a resposta que eu queria fazer. Eu definiria a especificação como "se o evento for neste dia do calendário, delta presente em horas. Caso contrário, use as datas apenas para determinar o delta" As horas de teste são úteis apenas dentro de um dia, se além disso sua resolução for de dias.
Baldrickk
11
Gosto dessa resposta porque aponta o problema real: pontos no tempo e datas são duas quantidades diferentes. Eles estão relacionados, mas quando você começa a compará-los, as coisas vão para o sul muito rapidamente. Na programação, a lógica de data e hora é uma das coisas mais difíceis de corrigir. Eu realmente não gosto que muitas implementações de data basicamente armazenem a data como um 0:00 ponto no tempo. Isso cria muita confusão.
Pieter B
38

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?

Ian Jacobs
fonte
2
Ter testes escritos por alguém que não seja o desenvolvedor é sempre uma boa ideia, isso significa que ambas as partes precisam ignorar a mesma condição de entrada para que o bug entre em produção.
Michael Kay
35

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.

cbojar
fonte
9
Essa é uma parte realmente importante do TDD que geralmente é negligenciada e que eu raramente vi mencionada em artigos e guias - é realmente importante testar casos extremos e condições de contorno, pois acho que essa é a fonte de 90% dos erros - of-by erros -ona mais e underflows, último dia do mês, último mês do ano, pulo anos etc etc
GoatInTheMachine
2
@GoatInTheMachine - e 90% desses bugs 90% estão em torno de transições horário de verão ..... Hahaha
Caleb
11
Você pode primeiro dividir as entradas possíveis em classes de equivalência e determinar os casos de borda nas bordas das classes. De nossa parte, é um esforço que pode ser maior que o esforço de desenvolvimento; se vale a pena depende de quão importante é entregar o software o mais livre de erros possível, qual é o prazo e quanto dinheiro e paciência você tem.
Peter - Restabelece Monica
2
Essa é a resposta correta. Muitas regras de negócios exigem que você divida um intervalo de valores em intervalos nos quais eles são casos para serem tratados de maneiras diferentes.
Abuzittin gillifirca
14

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).

Polygnome
fonte
12

A única maneira de pensar é adicionar muitas afirmações para os casos que eu acredito que nunca aconteceriam (como eu acreditava que um dia atrás é necessariamente ontem) e, em seguida, percorrer cada segundo nos últimos dez anos, verificando se há qualquer violação de afirmação, que parece muito complexa.

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 lista l, 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.

Hovercouch
fonte
2
Essa é exatamente a solução certa para esse tipo de problema. O conjunto de saídas válidas é fácil de definir (você pode fornecer uma expressão regular de maneira muito simples, algo assim /(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.
Jules
@Jules Veja também verificação / teste de propriedades . Normalmente escrevo testes de propriedades durante o desenvolvimento, para cobrir o maior número possível de casos imprevistos e forço-me a pensar em propriedades / invariantes gerais. I salvar testes pontuais para regressões e tal (que questão do autor é uma instância de)
Warbo
11
Se você fizer tanto esse loop nos testes, levará muito tempo, o que derrota um dos principais objetivos do teste de unidade: execute os testes rapidamente !
CJ Dennis
5

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.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

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 time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
Antonio Perez
fonte
5

O desenvolvimento orientado a testes não ajudou.

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 é:

  • Não escreva testes para confirmar se a função em teste funciona como você a fez. Faça testes que deliberadamente o quebram.

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.

Chris Becke
fonte
1

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:

  • Reunir requisitos completos nos permite saber como é o comportamento incorreto em nosso código.
  • Escrever código limpo e cuidadosamente arquitetado facilita evitar a introdução de erros e corrigi-los quando os identificamos.
  • Escrever testes nos permite produzir um registro do que acreditamos que muitos dos piores bugs possíveis em nosso software seriam e provar que evitamos pelo menos esses bugs. O TDD produz esses testes antes do código, o BDD deriva esses testes dos requisitos e o teste de unidade antiquado produz testes após a gravação do código, mas todos evitam as piores regressões no futuro.
  • Revisões por pares significam que toda vez que o código é alterado, pelo menos dois pares de olhos viram o código, diminuindo a frequência com que os bugs entram no master.
  • Usar um rastreador de bugs ou um rastreador de histórias de usuários que trate bugs como histórias de usuários significa que, quando os bugs aparecem, eles são rastreados e, em última análise, tratados, não esquecidos e deixados para ficar sempre no caminho dos usuários.
  • O uso de um servidor de temporariedade significa que, antes de uma versão principal, qualquer bug de interrupção de exibição tem a chance de aparecer e ser resolvido.
  • Usar o controle de versão significa que, na pior das hipóteses, em que o código com os principais erros é enviado aos clientes, você pode executar uma reversão de emergência e colocar um produto confiável nas mãos de seus clientes enquanto resolve as coisas.

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.

Kevin
fonte
1

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.

Simon
fonte
1

(e acreditando que isso tenha a ver com fusos horários, apesar do uso uniforme do UTC no código)

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.

Sergey
fonte
0
  • 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".

nulo
fonte
0

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:

Os requisitos eram relativamente claros

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!

CJ Dennis
fonte
0

É muito fácil cometer um erro lógico, mesmo em um código-fonte tão simples.

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.

O desenvolvimento orientado a testes não ajudou.

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.

Também preocupante é que não consigo ver como esses erros podem ser evitados.

Você não pode. Nem a NASA encontrou uma maneira de evitar bugs; nós, seres humanos inferiores, certamente também não.

Além de pensar mais antes de escrever código,

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).

a única maneira de pensar é adicionar muitas afirmações para os casos que eu acredito que nunca aconteceriam (como eu acreditava que um dia atrás é necessariamente ontem) e, em seguida, percorrer cada segundo nos últimos dez anos, verificando se há qualquer violação de afirmação, que parece muito complexa.

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.

Como eu poderia evitar criar esse bug em primeiro lugar?

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.

AnoE
fonte
-2

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.

gbjbaanb
fonte
Isso é extremamente derrotável. That in turn means you should spend less time using these techniques- mas você acabou de dizer que ajudará com menos erros ?!
JᴀʏMᴇᴇ
@ JᴀʏMᴇᴇ é mais uma atitude pragmática de qual técnica você ganha mais dinheiro.Eu conheço pessoas que se orgulham de passar 10 vezes escrevendo testes do que em seu código e ainda têm bugs Então, sendo mais sensato do que dogmático, sobre técnicas de teste é essencial. E os testes de integração precisam ser usados ​​de qualquer maneira, portanto, dedique mais esforço a eles do que ao teste de unidade.
Gbjbaanb