Criterio di copertura degli usi
Un test \(\ T\) soddisfa il criterio di copertura degli usi se e solo se per ogni nodo \(i\) e ogni variabile \(x\) appartenente a \(\operatorname{def}(i)\), \(T\) include un caso di test che esegue un cammino libero da definizioni da \(i\) ad ogni elemento di \(\operatorname{du}(i, \, x).\)
Sembra simile al precedente, con la differenza che ora bisogna coprire tutti i potenziali usi di una variabile definita. Questo appare ancora più chiaro osservando la formula matematica:
$$ \begin{align*} T \in C_{path} \Longleftrightarrow& \forall i \in P, \ \forall x \in \operatorname{def}(i), \ \forall j \in \operatorname{du}(i, \, x), \\ & \exists t \in T \ \text{che esegue un cammino da $i$ a $j$ senza ulteriori definizioni di $x$}. \end{align*} $$
Si noti però che il criterio di copertura degli usi non implica il criterio di copertura delle definizioni, perché nel caso in cui non esistano \(j \in \operatorname{du}(i, \, x)\) l’uso del \(\forall\) è più “permissivo” del \(\exists\) del criterio precedente: quest’ultimo richiedeva infatti che per ogni definizione esistesse almeno un uso, mentre il criterio di copertura degli usi non pone tale clausola (se non ci sono usi il \(\forall\) è sempre vero). Viene quindi da sé che questo criterio non copre neanche il criterio di copertura dei comandi.
Riconsideriamo nuovamente il programma in C visto in precedenza come esempio:
01 void main() {
02 float a, b, x, y;
03 read(x);
04 read(y);
05 a = x;
06 b = y;
07 while (a != b)
08 if (a > b)
09 a = a - b;
10 else
11 b = b - a;
12 write(a);
13 }
Come prima, consideriamo la variabile \(\mathtt a\) e i relativi insieme dei nodi degli usi per ogni sua definizione:
- \(\operatorname{du}(5, \, \mathtt a)\) = \({7, \, 8, \, 9, \, 11, \, 12}\);
- \(\operatorname{du}(9, \, \mathtt a)\) = \({7, \, 8, \, 9, \, 11, \, 12}\).
Per ogni definizione occorre coprire tutti gli usi:
\(\operatorname{du}(5, \, \mathtt a)\) |
\(\operatorname{du}(9, \, \mathtt a)\) |
---|---|
\(\D{5}\UG{7}\UG{8}\UG{11}\U{7}\UG{12}\) |
\(\dots \, \D{9} \UG7 \UG8 \UG9 \dots\) |
\(\dots \, \D5 \U7 \U8 \UG9 \dots\) |
\(\dots \, \D9 \U7 \U8 \UG{12} \dots\) |
\(\dots \, \D9 \U7 \U8 \UG{11} \dots\) |
Un test che soddisfa totalmente il criterio può essere il seguente:
$$ T = { \langle 4, \, 8 \rangle, \, \langle 12, \, 8 \rangle, \, \langle 12, \, 4 \rangle }. $$
Questo esempio permette di notare qualcosa sulla natura dei cicli: dovendo testare ogni percorso al loro interno è necessario fare almeno due iterazioni.
Può quindi sorgere un dubbio: è meglio che le due iterazioni siano fatte nello stesso caso di test o in casi test separati? Ovvero, è meglio minimizzare i casi di test o le iterazioni per caso?
Opinione diffusa è quella secondo cui è preferibile minimizzare le iterazioni: partizionando le casistiche in diversi casi di test è possibile rilevare con più precisione gli errori, riducendo il tempo di debug.
In alcune situazioni però aumentare il numero di iterazioni può diminuire il tempo di esecuzione totale dei test, in quanto dovendo riavviare il programma per ciascun caso di test la somma dei tempi di startup può diventare significativa per software molto massicci.