Qualunque programma informatico dovrebbe essere testato esaustivamente prima di essere rilasciato, tuttavia spesso si tende a minimizzare il significato del termine “esaustivo”, limitandosi a testare il proprio programma su un insieme estremamente limitato di input o condizioni.

Consideriamo ad esempio il seguente programma scritto in linguaggio F#, il linguaggio funzionale sviluppato da Microsoft Research; per maggiori informazioni sul linguaggio F# si faccia riferimento all’articolo: Introduzione al linguaggio F#, per maggiori informazioni sui linguaggi funzionali si faccia riferimento all’articolo: Programmazione Funzionale.

Come possiamo facilmente vedere il programma è responsabile della somma di due valori interi a e b:

let add a b = a + b;;
// val add : a:int -> b:int -> int

Come potremmo decidere di testarlo? Potremmo iniziare eseguendo alcune delle seguenti prove:

let testAdd a b r = match add a b with
                    |r -> true
                    |_ -> false;;
// val testAdd : a:int -> b:int -> r:'a -> bool

testAdd 1 1 2;;
// val it : bool = true

testAdd 2 2 4;;
// val it : bool = true

testAdd 50 10 60;;
// val it : bool = true

A questo punto dovremmo ritenerci soddisfatti, abbiamo dimostrato che la nostra espressione di somma funziona… oppure no? Quello che abbiamo dimostrato è semplicemente che presi in input due numeri interi la nostra espressione restituisce 2 se i valori in ingresso sono 1 e 1, restituisce 4 se i valori sono 2 e 2 e così via. Cosa succederebbe tuttavia se decidessimo di inserire due numeri negativi? Oppure un numero positivo e un numero negativo? E se il valore risultante fosse maggiore di 4294967295 (limite superiore dei valori interi)? E se gli input non fossero numeri interi, cosa succederebbe se fossero numeri in virgola mobile oppure stringe? Non abbiamo considerato nessuna di queste possibilità.

All’interno dei team incaricati di sviluppare grandi software può capitare (anzi, sarebbe proprio meglio che capitasse) che il testing dei frammenti di codice venga eseguito da una o più figure esterne (o comunque lontane) dallo sviluppo del software stesso. È buona regola infatti che un programmatore non svolga direttamente il test dei propri programmi ma che questo compito venga svolto da una figura esterna al suo sviluppo, in questo modo c’è una maggiore probabilità che il tester (la figura incaricata di svolgere questo compito) riesca ad identificare un numero maggiore di anomalie all’interno del programma. Potremmo parlare per ore delle metodologie più corrette da utilizzare per svolgere il testing dei programmi software, tuttavia non è questa la sede giusta dove discutere di questo argomento, limitiamoci quindi per a considerare il nostro programma d’esempio scritto in linguaggio F#.

Supponiamo di essere la persona incaricata di svolgere il test dell’espressione “add” e di essere quasi completamente all’oscuro dell’implementazione della stessa, sappiamo solamente che l’espressione è stata sviluppata in modo da accettare due input e restituire in output la loro somma. Per testare il funzionamento dell’espressione potremmo limitarci ad eseguire:

let testAdd a b t = match add a b with
                    |t -> true
                    |_ -> false;;
// val testAdd : a:int -> b:int -> t:'a -> bool

testAdd 1 2 3;;

Tuttavia come potremmo salvaguardarci dalla (s)fortunata implementazione del nostro caro collega programmatore?

let add a b =
    if a=1 && b=2 then 
        3
    else
        0;;

È chiaro che dobbiamo svolgere qualche test in più.