Come scegliere i casi di test
Abbiamo barbellato a lungo sui criteri di copertura, ed è giunta l'ora di vederne un po'.
Criterio di copertura uniforme dello spazio di input
Sappiamo già bene che non è possibile in pratica coprire tutto lo spazio di input, anche per i casi più semplici. Potremmo decidere di generare a caso valori di input, ma per essere un pochettino più furbi utilizziamo invece una distribuzione uniforme dei casi di test rispetto allo spazio di input.
Ad esempio, se ho a disposizione 10 minuti, e ogni minuto riesco a fare 6000 test, allora dividerò lo spazio di input complessivo in 6000 * 10 parti. Se la funzione prende come valore in input un intero (232), allora faccio 2'^32 / 60000, e di ciascuna "fettina" così ricavata prendo il valore centrale.
Valori tipici e critici
Tuttavia, usare pedissequamente la copertura uniforme non ci permette di mettere nella dovuta considerazione la faccenda dei valori tipici e critici, vista nella precedente lezione.
Per sistemare questo problema, stabiliamo che se la nostra fettina dello spazio in input è una fettina che sta sul bordo, allora considereremo più importanti i valori sul bordo piuttosto che quelli che stanno in mezzo alla fettina. Se invece la fettina sta nel mezzo, allora ci comportiamo come prima ed utilizziamo il suo punto centrale.
Dobbiamo stare tuttavia attenti a non concentrare tutto lo sforzo sui valori del boundary, perché potrebbero essere veramente tanti ed esaurire il tempo a nostra disposizione.
Anche prima parlavamo di tempo: nel mondo reale c'è solo un tempo limitato che si può dedicare al testing, e pertanto va sfruttato nel modo più intelligente possibile.
Sfruttare le pre- e le post-condizioni
Durante la fase di modellazione potrei aver avuto modo di stabilire, per ogni metodo, delle precondizioni e delle postcondizioni. Le precondizioni sono quelle condizioni che devono valere prima della chiamata di un metodo. Le postcondizioni sono quelle condizioni che invece devono valre dopo l'esecuzione di un metodo.
Quando stabilisco a priori queste condizioni, si dice che faccio una verifica del modello. Controllare queste condizioni a posteriori si chiama invece validazione. Le persone che si occupano di generare modelli dotati di semantica (cioè, in ultima analisi, di pre- e postcondizioni) in questo caso possono essere le stesse che genereranno il test plan, proprio perché sfrutto le informazioni che il modello mi dà per generare casi di test. L'attività si chiama allora Verification and Validation, V&V in breve.
All'atto pratico, utilizzerò come casi di test quei valori che generano una violazione delle precondizioni, perché sarà lì che il mio codice presumibilmente cascherà.
Test non funzionali
All'inizio dicevamo che i test non funzionali si eseguono perlopiù con tecniche whitebox, cioè tecniche che applicano criteri di copertura basati sul codice scritto.
Il criterio di copertura si può definire come il numero di tracce esecutive che il mio caso di test esegue con un certo valore, rispetto al numero di tracce esecutive totale.
Una traccia esecutiva è un "percorso" all'interno del mio codice. Se immaginiamo il nostro codice sotto forma di diagramma di flusso, allora una traccia esecutiva è un percorso in questo diagramma che parte dall'inizio e arriva alla fine. Una traccia può coinvolgere o meno tutte le parti di questo diagramma, e proprio la quantità di questa copertura è il mio criterio.
In realtà siamo un po' più specifici, e consideriamo questi aspetti di copertura del codice:
- statement coverage = quante istruzioni singole sono state eseguite
- loop coverage = quanti loop ho eseguito
- branch coverage = quante branch, ovvero rami di una condizione, ho coperto
- path coverage = quante tracce esecutive ho percorso
Spesso queste definizioni sono intercambiabili: ad esempio, una copertura completa delle branch è sicuramente una copertura completa del codice. Tuttavia servono per focalizzarsi meglio sulla parte di codice che voglio testare.
Altri tipi di test non funzionali
Non si testa solamente il codice, ci sono anche altri requisiti non funzionali che possono essere oggetto di testing.
- configuration testing = si cambiano le configurazioni del programma, e si vede se si comporta ancora bene
- recovery testing = si verifica il comportamento del sw dopo un crash e successivo recovery
- safety testing = si verifica se il programma rispetta i permessi dei dati che manipola
- security testing = si verifica se il programma resiste ad attacchi maliziosi
- stress testing = si verifica il comportamento del programma nelle condizioni di lavoro più onerose, dal punto di vista delle performance
- performance testing = si verifica il comportamento del programma, sempre dal punto di vista delle performance, ma nel caso di carico di lavoro medio
Test di utilità
Un test di utilità mi dice quanto il prodotto soddisfa le esigenze dell'utente, così come previsto dalle specifiche.
Sicuramente, implica la completezza funzionale: se non fa quello che deve, allora sicuramente non gli piace. Ma occorre anche vedere come fa quello che deve fare.
Si deve quindi anche considerare la facilità d'uso, o l'accessibilità, tenendo a mente che questa dipende da chi utilizzerà il programma. Gli utenti potranno essere diversi, e potranno avere una cultura diversa, attitudini diverse, approcci diversi allo stesso problema, pur utilizzando lo stesso software. Ci si aspetta che il nostro prodotto rispetti queste differenze interne all'ambiente lavorativo in cui sarà installato
Infine, c'è da valutare anche il rapporto costi/funzionalità.
Note sulla correttezza di un programma
Un sw è corretto quando, usato secondo le specifiche, soddisfa le specifiche stesse.
Qui però salta fuori la distinzione tra fault e failure. Prendiamo ad esempio il seguente frammento di codice C:
if (c = 0) { ... }
La condizione è stata scritta c = 0, cioè si tratta di un assegnamento, il cui risultato sarà sempre true.
Se l'utilizzo solito del programma prevede che a quel punto la variabile c valga effettivamente 0, allora non ci accorgeremo mai dell'errore, perché si comporta sempre come dovrebbe! Se infatti c valesse 0, la condizione corretta c == 0 darebbe true, esattamente come già dà ora l'assegnamento c = 0. Pertanto non ho una failure, pur avendo un fault nel codice. La failure è in agguato nell'oscurità!