Não posso apontar um bom recurso on-line (os artigos da Wikipedia em inglês sobre esses tópicos tendem a ser improváveis), mas posso resumir uma palestra que ouvi que também abordou a teoria básica dos testes.
Modos de teste
Existem diferentes classes de testes, como testes de unidade ou testes de integração . Um teste de unidade afirma que um pedaço de código coerente (função, classe, módulo) obtido por conta própria funciona como esperado, enquanto um teste de integração afirma que vários desses pedaços funcionam corretamente juntos.
Um caso de teste é um ambiente conhecido no qual uma parte do código é executada, por exemplo, usando entrada de teste específica ou zombando de outras classes. O comportamento do código é então comparado ao comportamento esperado, por exemplo, um valor de retorno específico.
Um teste pode apenas provar a presença de um erro, nunca a ausência de todos os erros. Os testes colocam um limite superior na correção do programa.
Cobertura de código
Para definir métricas de cobertura de código, o código-fonte pode ser convertido em um gráfico de fluxo de controle em que cada nó contém um segmento linear do código. O controle flui entre esses nós apenas no final de cada bloco e é sempre condicional (se condição, então vá para o nó A, depois vá para o nó B). O gráfico possui um nó inicial e um nó final.
- Com este gráfico, a cobertura de instrução é a proporção de todos os nós visitados para todos os nós. A cobertura completa da declaração não é suficiente para testes completos.
- A cobertura da filial é a proporção de todas as arestas visitadas entre os nós no CFG e todas as arestas. Isso insuficientemente testa loops.
- A cobertura do caminho é a proporção de todos os caminhos visitados para todos os caminhos, onde um caminho é qualquer sequência de arestas do nó inicial ao final. O problema é que, com loops, pode haver um número infinito de caminhos; portanto, a cobertura completa do caminho não pode ser praticamente testada.
Portanto, geralmente é útil verificar a cobertura da condição .
- Na cobertura de condições simples , cada condição atômica é verdadeira e falsa - mas isso não garante a cobertura completa da declaração.
- Na cobertura de várias condições , as condições atômicas assumiram todas as combinações de
true
e false
. Isso implica cobertura total da agência, mas é bastante caro. O programa pode ter restrições adicionais que excluem determinadas combinações. Essa técnica é boa para obter cobertura de ramificação, pode encontrar código morto, mas não consegue encontrar erros decorrentes da condição incorreta .
- Na cobertura mínima de múltiplas condições , cada condição atômica e composta é verdadeira e falsa. Ainda implica uma cobertura completa da agência. É um subconjunto de várias condições de cobertura, mas requer menos casos de teste.
Ao construir a entrada de teste usando a cobertura da condição, o curto-circuito deve ser levado em consideração. Por exemplo,
function foo(A, B) {
if (A && B) x()
else y()
}
precisa ser testado com foo(false, whatever)
, foo(true, false)
e foo(true, true)
para a cobertura condição múltipla mínima completo.
Se você possui objetos que podem estar em vários estados, o teste de todas as transições de estado análogas ao controle de fluxos parece sensato.
Existem algumas métricas de cobertura mais complexas, mas geralmente são semelhantes às métricas apresentadas aqui.
Estes são métodos de teste de caixa branca e podem ser parcialmente automatizados. Observe que um conjunto de testes de unidade deve ter uma cobertura de código alta por qualquer métrica escolhida, mas 100% nem sempre é possível. É especialmente difícil testar o tratamento de exceções, onde as falhas precisam ser injetadas em locais específicos.
Testes funcionais
Depois, há testes funcionais que afirmam que o código adere às especificações, visualizando a implementação como uma caixa preta. Esses testes são úteis para testes de unidade e testes de integração. Como é impossível testar com todos os dados de entrada possíveis (por exemplo, testar o comprimento da string com todas as strings possíveis), é útil agrupar a entrada (e a saída) em classes equivalentes - se length("foo")
estiver correto, foo("bar")
provavelmente funcionará também. Para cada combinação possível entre as classes de equivalência de entrada e saída, pelo menos uma entrada representativa é escolhida e testada.
Deve-se testar adicionalmente
- casos extremos
length("")
, foo("x")
, length(longer_than_INT_MAX)
,
- valores permitidos pelo idioma, mas não pelo contrato da função
length(null)
, e
- possíveis dados indesejados
length("null byte in \x00 the middle")
...
Com números, isso significa testar 0, ±1, ±x, MAX, MIN, ±∞, NaN
e com comparações de ponto flutuante testando dois carros alegóricos vizinhos. Como outra adição, valores de teste aleatórios podem ser selecionados nas classes de equivalência. Para facilitar a depuração, vale a pena registrar a semente usada…
Testes não funcionais: testes de carga, testes de estresse
Um software possui requisitos não funcionais, que também precisam ser testados. Isso inclui testes nos limites definidos (testes de carga) e além deles (testes de estresse). Para um jogo de computador, isso pode estar afirmando um número mínimo de quadros por segundo em um teste de carga. Um site pode ser submetido a um teste de estresse para observar os tempos de resposta quando o dobro do número de visitantes antecipados está prejudicando os servidores. Esses testes não são relevantes apenas para sistemas inteiros, mas também para entidades únicas - como uma tabela de hash é degradada com um milhão de entradas?
Outros tipos de testes são testes de todo o sistema em que cenários são simulados ou testes de aceitação para provar que o contrato de desenvolvimento foi cumprido.
Métodos sem teste
Avaliações
Existem técnicas que não são de teste que podem ser usadas para garantir a qualidade. Exemplos são orientações, revisões formais de código ou programação de pares. Embora algumas peças possam ser automatizadas (por exemplo, usando linters), elas geralmente demandam muito tempo. No entanto, as revisões de código por programadores experientes têm uma alta taxa de descoberta de bugs e são especialmente valiosas durante o design, onde nenhum teste automatizado é possível.
Quando as revisões de código são tão boas, por que ainda escrevemos testes? A grande vantagem dos conjuntos de testes é que eles podem ser executados (principalmente) automaticamente e, portanto, são muito úteis para testes de regressão .
Verificação formal
A verificação formal confirma e prova certas propriedades do código. A verificação manual é principalmente viável para partes críticas, menos para programas inteiros. As provas colocam um limite inferior na correção do programa. As provas podem ser automatizadas até certo ponto, por exemplo, através de um verificador de tipo estático.
Certos invariantes podem ser verificados explicitamente usando assert
instruções.
Todas essas técnicas têm seu lugar e são complementares. O TDD grava os testes funcionais antecipadamente, mas os testes podem ser julgados por suas métricas de cobertura assim que o código for implementado.
Escrever código testável significa escrever pequenas unidades de código que podem ser testadas separadamente (funções auxiliares com granularidade adequada, princípio de responsabilidade única). Quanto menos argumentos cada função receber, melhor. Esse código também se presta à inserção de objetos simulados, por exemplo, via injeção de dependência.
double pihole(double value) { return (value - Math.PI) / (value - Math.PI); }
que aprendi com meu professor de matemática . Esse código tem exatamente um buraco , que não pode ser descoberto automaticamente apenas a partir de testes em caixas pretas. Em Math, não existe esse buraco. No cálculo, você pode fechar o buraco se os limites unilaterais forem iguais.