Torna alla pagina di Ingegneria del Software
:: Ingegneria del Software - Appunti dell'12 Maggio 2009 ::
Quest'oggi facciamo un po' di esercizi sul testing.
Una domanda classica da esame consiste nel somministrarci del codice, e chiederci come genereremmo i casi di test senza sapere niente della semantica, oppure come li genereremmo sapendo invece qualcosa della semantica.
Per esempio, ci viene dato questo codice:
void swap (int &x, int &y) { /*swaps x,y if x > y*/ ... }
Beh, il codice non c'è, però abbiamo un commento che ci dice che cosa dovrebbe fare quella funzione.
L'interfaccia del metodo mi dice che prende in ingresso due interi, o meglio, due reference ad interi. Il metodo della suddivisione uniforme dello spazio di input vuole che noi grigliamo a dovere lo spazio di input, e prendiamo dei valori tendenzialmente distribuiti sui bordi di tale spazio. Ovviamente non è il massimo della furbizia.
Un essere umano è in grado di leggere il commento e di trarre informazioni sul contenuto. Capirebbe subito che il metodo uniforme non serve quasi a niente, e allora decide di suddividere i suoi 10 test che ha il tempo di fare così:
Ovviamente, fa sempre comodo prendere i valori sul boundary, ma tutti gli altri casi di test li prende con un occhio alla condizione espressa nel commento.
Esistono anche i generatori automatici di test. Questi prendono delle precondizioni scritte in un linguaggio formale, e traggono automaticamente dei casi di test. In questo esempio, non ci sono, e quindi ci attacchiamo.
Finora siamo stati in grado di prendere la "metà" dello spazio di input o simili cose perché lo spazio di input stesso era costituito da numeri. Nel caso in cui i tipi in ingresso non siano ordinali, che ordinamento posso prendere? La risposta semplice e funzionale non c'è. Elenchiamo tuttavia alcuni metodi:
ma si capisce subito che non sono soluzioni generali al problema.
Abbiamo una funzione boolean Bisestile(anno) la quale ci dice se un anno è bisestile o no.
Per sapere se l'anno è bisestile, queste sono le regole:
Siccome ho tutte queste belle informazioni sulla definizione del metodo, non vado a cercare il boundary, ma cerco di sfruttare queste condizioni. Vediamole in forma più matematica:
((x % 4 == 0) AND (x % 100 != 0)) OR (x % 400 == 0)
Ho due decisioni:
e 3 condizioni, che sarebbero le singole parentesi.
Per testare il tutto, dovremmo prendere almeno i seguenti casi di test:
Questo, in linea teorica.
Dal punto di vista pratico, possiamo osservare che se un numero non è divisibile per 100, non lo sarà nemmeno per 4, e nemmeno per 400. Le condizioni non sono totalmente indipendenti tra di loro.
Pertanto, possiamo scegliere:
Per avere qualche indizio in più relativo al procedimento da seguire in questi frangenti si può consultare la pagina di LPS dedicata al testing.
Il metodo bisestile è implementato così:
if (a % 400 == 0) { if (a % 100 != 0) return true; else return false; } else if (a % 100 ==0) return false; else if (a % 4 == 0) return true; else return false;
Un po' convoluto.
L'esercizio ci chiede:
Lo statement coverage al 100% è irraggiungibile. La risposta va motivata, ed eccone la ragione: se un numero È divisibile per 400, il primo if è soddisfatto, ma sicuramente NON sarà soddisfatto l'if immediatamente successivo, quello che dice if (a % 100 != 0), perché se un numero è divisibile per 400 allora lo è anche per 100. Pertanto, l'istruzione return true di quella riga non verrà mai eseguita.
Il path coverage invece lo posso avere al 100%: infatti, i cammini esecutivi sono quelli effettivamente esguibili. Quelli visti sopra non sono eseguibili, ma tutti gli altri li posso eseguire tranquillamente.
Il codice che non può mai essere raggiunto si chiama dead code, e la ragione della sua esistenza si deve alla presenza di una condizione interna che nega una condizione presente sul cammino compiuto per arrivare ad essa.