Saltar para o conteúdo
Topos Uranos

Resumo

Um programa que compila e escreve algo razoável não está, por isso, testado. Esta aula ensina a testar com método: a derivar os casos da especificação, por regiões e bordas; a reuni-los num programa de testes automáticos que conta as falhas e as informa; a verificar com assert o que não pode ocorrer se o programa é correto, e com static_assert, em tempo de compilação, o que uma função constexpr deve calcular; e a executar os testes com os detectores, que descobrem o comportamento indefinido que um resultado correto oculta. Tudo repousa sobre uma advertência: um teste pode mostrar a presença de um erro, nunca a sua ausência.

Objetivos de aprendizagem

  1. Derivar os casos de teste da especificação de uma função: as partições da sua entrada, as bordas entre elas e os casos especiais, com o resultado que a pós-condição exige em cada um.
  2. Escrever um programa de testes automáticos que compare os resultados obtidos com os esperados, informe cada falha sem parar e termine com um resumo e um código de saída que o reflita.
  3. Usar assert para verificar pré-condições e invariantes, e explicar por que desaparece ao compilar com -DNDEBUG e por que a sua condição não deve ter efeitos colaterais.
  4. Usar static_assert com funções constexpr para testar resultados em tempo de compilação, e executar os testes com os detectores para descobrir o comportamento indefinido que os resultados não revelam.

O que é testar

Testar um programa é executá-lo com entradas escolhidas e comparar o que ele faz com o que deveria fazer. Cada caso de teste consta de duas partes: uma entrada que cumpre a pré-condição, e o resultado que a pós-condição exige para ela, calculado de antemão e por outro caminho (à mão, com outra fórmula, com uma tabela conhecida). Sem o resultado esperado não há teste, mas uma execução: olhar a saída e achá-la “razoável” é uma impressão, não uma comparação.

Ora, uma função de um argumento int admite mais de quatro bilhões de entradas, e uma de dois argumentos, cerca de 1,8⋅10191{,}8 \cdot 10^{19} pares; nenhuma bateria de testes percorre todas. Se isso é assim, os testes examinam uma parte ínfima dos casos possíveis, e do fato de que todos passem não se segue que a função seja correta. Edsger Dijkstra formulou isso em 1970: os testes podem mostrar a presença de erros, nunca a sua ausência. A distinção é a que o curso de Lógica Matemática I estabelece entre verificar casos e demonstrar: verificar que uma fórmula é verdadeira para alguns valores não prova que o seja para todos, ao passo que uma demonstração (neste curso, o invariante da aula Projetar laços corretos) cobre todos os casos de uma vez. Os testes não substituem esse raciocínio: encontram os erros que ele deixou passar.

Disso se segue o critério para escolher os casos. Se os testes não podem confirmar a correção, o seu valor está em encontrar erros, e um bom caso de teste é o que tem mais probabilidade de falhar. Um caso que o autor escolheu porque sabia que funcionaria não informa nada. O programa seguinte o mostra com uma função que deve devolver o maior elemento de um vetor não vazio.

Programa em C++
#include <print>#include <vector>​// Maior elemento de uma lista de dados.// Pré-condição: values não está vazio.// Pós-condição: devolve o maior elemento de values.int largest(const std::vector<int>& values){    int best{0};    for (const int v : values) {        if (v > best) {            best = v;        }    }    return best;}​int main(){    std::println("{}", largest({3, 9, 4}));    std::println("{}", largest({7}));    std::println("{}", largest({1, 2, 3, 4}));    std::println("{}", largest({-5, -2, -8}));}
Saída
9740

Os três primeiros casos são os que qualquer um escreve primeiro, e a função passa neles: 9, 7 e 4 estão corretos. O quarto dá 0, um valor que nem sequer está no vetor, quando deveria dar −2-2. O erro está na linha 9: best começa em 0 e não no primeiro elemento, de modo que a função é correta só se algum elemento for positivo ou zero. Os casos típicos compartilhavam essa propriedade sem que ninguém a escolhesse; o quarto a viola de propósito. A correção é int best{values[0]};, que a pré-condição torna legítima, porque garante que values[0] existe.