Vai al contenuto
Topos Uranos

Riepilogo

Un programma che compila e scrive qualcosa di ragionevole non è, per questo, testato. Questa lezione insegna a testare con metodo: a derivare i casi dalla specifica, per regioni e bordi; a riunirli in un programma di test automatici che conta i fallimenti e li segnala; a verificare con assert ciò che non può accadere se il programma è corretto, e con static_assert, in fase di compilazione, ciò che una funzione constexpr deve calcolare; e a eseguire i test con i sanitizer, che scoprono il comportamento indefinito che un risultato corretto nasconde. Tutto poggia su un avvertimento: un test può mostrare la presenza di un errore, mai la sua assenza.

Obiettivi di apprendimento

  1. Derivare i casi di test dalla specifica di una funzione: le partizioni del suo input, i bordi tra di esse e i casi speciali, con il risultato che la postcondizione esige in ciascuno.
  2. Scrivere un programma di test automatici che confronti i risultati ottenuti con quelli attesi, segnali ogni fallimento senza fermarsi e termini con un riepilogo e un codice di terminazione che lo rifletta.
  3. Usare assert per verificare precondizioni e invarianti, e spiegare perché scompare quando si compila con -DNDEBUG e perché la sua condizione non deve avere effetti collaterali.
  4. Usare static_assert con funzioni constexpr per testare risultati in fase di compilazione, ed eseguire i test con i sanitizer per scoprire il comportamento indefinito che i risultati non rivelano.

Che cos’è testare

Testare un programma significa eseguirlo con input scelti e confrontare ciò che fa con ciò che avrebbe dovuto fare. Ogni caso di test consta di due parti: un input che soddisfa la precondizione, e il risultato che la postcondizione esige per esso, calcolato in anticipo e per un’altra via (a mano, con un’altra formula, con una tabella nota). Senza il risultato atteso non c’è test, ma soltanto un’esecuzione: guardare l’output e trovarlo «ragionevole» è un’impressione, non un confronto.

Ora, una funzione di un argomento int ammette più di quattro miliardi di input, e una di due argomenti circa 1,8⋅10191{,}8 \cdot 10^{19} coppie; nessuna batteria di test li percorre tutti. Se è così, i test esaminano una parte infima dei casi possibili, e dal fatto che passino tutti non segue che la funzione sia corretta. Edsger Dijkstra lo formulò nel 1970: i test possono mostrare la presenza di errori, mai la loro assenza. La distinzione è quella che il corso Logica Matematica I stabilisce tra verificare casi e dimostrare: verificare che una formula è vera per alcuni valori non prova che lo sia per tutti, mentre una dimostrazione (in questo corso, l’invariante della lezione Progettare cicli corretti) copre tutti i casi in una volta. I test non sostituiscono quel ragionamento: trovano gli errori che gli sono sfuggiti.

Ne segue il criterio per scegliere i casi. Se i test non possono confermare la correttezza, il loro valore sta nel trovare errori, e un buon caso di test è quello che ha più probabilità di fallire. Un caso scelto sapendo che avrebbe funzionato non dice nulla. Il programma seguente lo mostra con una funzione che deve restituire l’elemento più grande di un vettore non vuoto.

Programma in C++
#include <print>#include <vector>​// Elemento più grande di un elenco di dati.// Precondizione: values non è vuoto.// Postcondizione: restituisce l'elemento più grande di 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}));}
Output
9740

I primi tre casi sono quelli che chiunque scrive per primi, e la funzione li supera: 9, 7 e 4 sono corretti. Il quarto dà 0, un valore che non è nemmeno nel vettore, quando avrebbe dovuto dare −2-2. L’errore è alla riga 9: best parte da 0 e non dal primo elemento, sicché la funzione è corretta solo se qualche elemento è positivo o zero. I casi tipici condividevano questa proprietà senza che nessuno l’avesse scelta; il quarto la viola di proposito. La correzione è int best{values[0]};, che la precondizione rende legittima, perché garantisce che values[0] esiste.