Pergunta

Como programador, comprei de todo o coração na filosofia do TDD e me esforcei para fazer extensos testes de unidade para qualquer código não trivial que escrevo. Às vezes, essa estrada pode ser dolorosa (mudanças comportamentais causando cascata de múltiplas alterações de teste de unidade; grandes quantidades de andaimes necessárias), mas, em geral resultado.

Recentemente, eu tenho jogado com Haskell, e sua biblioteca de testes residentes, QuickCheck. De uma maneira distintamente diferente do TDD, o Quickcheck tem ênfase no teste de invariantes do código, ou seja, certas propriedades que mantêm sobre todos (ou subconjuntos substantivos) de insumos. Um exemplo rápido: um algoritmo de classificação estável deve dar a mesma resposta se executá -lo duas vezes, deve ter uma saída crescente, deve ser uma permutação da entrada, etc. Então, o QuickCheck gera uma variedade de dados aleatórios para testar esses invariantes.

Parece-me, pelo menos para funções puras (ou seja, funções sem efeitos colaterais-e se você zombar corretamente, poderá converter funções sujas em puras), que os testes invariantes podem suplantar testes de unidade como um superconjunto estrito dessas capacidades . Cada teste de unidade consiste em uma entrada e uma saída (em linguagens de programação imperativa, a "saída" não é apenas o retorno da função, mas também qualquer estado alterado, mas isso pode ser encapsulado). É possível criar um gerador de entrada aleatório que seja bom o suficiente para cobrir todas as entradas de teste de unidade que você criaria manualmente (e mais alguns, porque isso geraria casos em que você não teria pensado); Se você encontrar um bug no seu programa devido a alguma condição de contorno, melhore seu gerador de entrada aleatório para que ele gerem esse caso também.

O desafio, então, é se é possível formular ou não invariantes úteis para todos os problemas. Eu diria que é: é muito mais simples quando você tiver uma resposta para ver se está correto do que para calcular a resposta em primeiro lugar. Pensar em invariantes também ajuda a esclarecer a especificação de um algoritmo complexo muito melhor do que os casos de teste ad hoc, que incentivam um tipo de pensamento caso a caso do problema. Você pode usar uma versão anterior do seu programa como uma implementação de modelo ou uma versão de um programa em outro idioma. Etc. Eventualmente, você pode cobrir todas as suas casos de teste anteriores sem precisar codificar explicitamente uma entrada ou uma saída.

Eu fiquei louco ou estou em alguma coisa?

Foi útil?

Solução

Um ano depois, agora acho que tenho uma resposta para esta pergunta: Não! Em particular, os testes de unidade sempre serão necessários e úteis para testes de regressão, nos quais um teste é anexado a um relatório de bug e vive na base de código para impedir que esse bug volte.

No entanto, suspeito que qualquer teste de unidade possa ser substituído por um teste cujas entradas são geradas aleatoriamente. Mesmo no caso do código imperativo, a “entrada” é a ordem das declarações imperativas que você precisa fazer. Obviamente, se vale a pena criar ou não o gerador de dados aleatório e se você pode ou não fazer com que o gerador de dados aleatório tenha a distribuição certa é outra pergunta. O teste de unidade é simplesmente um caso degenerado em que o gerador aleatório sempre dá o mesmo resultado.

Outras dicas

O que você trouxe é um ponto muito bom - quando aplicado apenas à programação funcional. Você afirmou um meio de realizar tudo isso com código imperativo, mas também abordou por que não foi feito - não é particularmente fácil.

Eu acho que é esse o motivo pelo qual não substituirá o teste de unidade: não se encaixa no código imperativo tão facilmente.

Duvidoso

Eu só ouvi falar de (não usei) esses tipos de testes, mas vejo dois problemas em potencial. Eu adoraria ter comentários sobre cada um.

Resultados enganosos

Eu ouvi falar de testes como:

  • reverse(reverse(list)) deve ser igual list
  • unzip(zip(data)) deve ser igual data

Seria ótimo saber que eles são verdadeiros para uma ampla gama de insumos. Mas ambos os testes passariam se as funções retornassem sua entrada.

Parece -me que você gostaria de verificar isso, por exemplo, reverse([1 2 3]) é igual a [3 2 1] Para provar o comportamento correto em pelo menos um caso, então adicionar Alguns testes com dados aleatórios.

Complexidade de teste

Um teste invariante que descreve completamente a relação entre a entrada e a saída pode ser mais complexa que a própria função. Se for complexo, pode ser um buggy, mas você não tem testes para seus testes.

Um bom teste de unidade, por outro lado, é simples demais para estragar ou entender mal como leitor. Apenas um erro de digitação poderia criar um bug em "Espere reverse([1 2 3]) para igual [3 2 1]".

O que você escreveu em sua postagem original, lembrou -me desse problema, que é uma pergunta em aberto sobre o que o invariante do loop é provar o loop correto ...

De qualquer forma, não tenho certeza do quanto você leu em especificações formais, mas está descendo essa linha de pensamento. O livro de David Gries é um dos clássicos sobre o assunto, ainda não dominei o conceito o suficiente para usá -lo rapidamente na minha programação diária. A resposta usual às especificações formais é, é difícil e complicada, e só vale o esforço se você estiver trabalhando em sistemas críticos de segurança. Mas acho que existem técnicas de envelope semelhantes às exposições do QuickCheck que podem ser usadas.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top