Introduzione
Le proposte di test si basano su modelli di errore, nozioni relative agli errori plausibili nel software e alle
modalità con cui è possibile individuare tali errori. Questa linea guida spiega come creare proposte di test da
espressioni booleane e relazionali. Innanzitutto motiva le tecniche esaminando il codice, quindi descrive come
applicarle se il codice non è ancora stato scritto o non è in qualche modo disponibile.
Espressioni booleane
Considerare il seguente frammento di codice, preso da un sistema (immaginario) per la gestione della detonazione di una
bomba. Fa parte del sistema di sicurezza e controlla se viene rispettato il pulsante che consente di far esplodere la
bomba.
if (publicIsClear || technicianClear) {
bomb.detonate();
}
Il codice non è corretto. || dovrebbe essere &&. Tale errore avrà
effetti deleteri. Anziché far esplodere la bomba quando il pubblico e il tecnico della bomba sono chiari, il sistema
eseguirà la detonazione quando uno dei due è chiaro.
Quale test rileverà questo bug?
Si consideri un test in cui il pulsante viene premuto quando sia il tecnico che il pubblico sono chiari. Il codice
consentirà la detonazione della bomba. Ma, e questo è l'aspetto importante, il codice corretto (quello che
utilizza &&) farebbe lo stesso. Quindi il test è inutile per la rilevazione di questo
errore.
In modo simile, questo codice errato funziona correttamente quando sia il pubblico che il tecnico sono nei pressi della
bomba: la bomba non viene detonata.
Per rilevare il bug, è necessario che si verifichi un caso in cui il codice, così scritto, si esprime in modo
differente rispetto al codice che andava scritto. Ad esempio, il pubblico deve essere chiaro, ma il tecnico della bomba
è ancora accanto ad essa. Ecco tutti i test sotto forma di tabella:
publicIsClear
|
technicianClear
|
Il codice come è stato scritto...
|
Il codice corretto avrebbe dovuto...
|
|
true
|
true
|
detona
|
detonare
|
il test è inutile (per questo errore)
|
true
|
false
|
detona
|
non denotare
|
test utile
|
false
|
true
|
detona
|
non denotare
|
test utile
|
false
|
false
|
non detona
|
non denotare
|
il test è inutile (per questo errore)
|
Entrambi i due test centrali sono utili per la rilevazione di questo particolare errore. Tenere presente, tuttavia, che
sono ridondanti: dal momento che ambedue rileveranno l'errore, non sarà necessario eseguirli entrambi.
Vi sono altri modi in cui l'espressione potrebbe essere sbagliata. Di seguito sono riportati due elenchi di errori
comuni nelle espressioni booleane. Gli errori a sinistra vengono rilevati tutti tramite la tecnica discussa in questa
sezione. Gli errori a destra potrebbero non esserlo. Quindi questa tecnica non rileva tutti gli errori, ma è comunque
utile.
Rilevati errori
|
Errori probabilmente non rilevati
|
Utilizzo dell'operatore sbagliato: a || b dovrebbe essere a&& b
|
Utilizzata variabile sbagliata: a&&b&&c dovrebbe
essere a&& x&&d
|
Negazione omessa o non corretta: a||b dovrebbe essere !a||b o ! a||b dovrebbe essere a||b
|
L'espressione è troppo semplice: a&&b dovrebbe essere a&&b&&c
|
Le espressioni tra parentesi non sono configurate correttamente: a&&b||c dovrebbe essere
a&&(b||c)
|
Espressioni con più errori nella colonna di sinistra
|
L'espressione è eccessivamente complessa: a&&b&&c dovrebbe essere a&&b
(Questo errore non è così comune, ma è semplice da individuare con test utili per altri
motivi).
|
|
In che modo vengono utilizzate tali proposte? Si supponga di avere un'espressione booleana del tipo a&&!b. E' possibile costruire una tabella di valori true/false simile alla seguente:
a
|
b
|
a&&!b
(codice come è scritto)
|
forse dovrebbe essere
a||!b
|
forse dovrebbe essere
! a&&!b
|
forse dovrebbe essere
a&&b
|
...
|
true
|
true
|
false
|
true
|
false
|
true
|
...
|
true
|
false
|
true
|
true
|
false
|
false
|
...
|
false
|
true
|
false
|
false
|
false
|
false
|
...
|
false
|
false
|
false
|
true
|
true
|
false
|
...
|
Se sono state valutate tutte le possibilità, si rileverà che la prima, la seconda e la quarta sono le uniche
necessarie. La terza espressione non rileverà alcun errore che non possa essere rilevato da una delle altre, quindi non
è necessaria. (Man mano che le espressioni si complicano, il risparmio dovuto a casi non necessari aumenta
rapidamente).
Ovviamente, nessuno con un minimo di cervello creerebbe tale tabella. Fortunatamente, non è necessario farlo. E'
semplice memorizzare i casi necessari per le espressioni semplici. (Consultare la sezione successiva). Per espressioni
più complesse, ad esempio A&&B||C, consultare Proposte di test per combinazioni di AND e OR, che elenca le proposte di test per
espressioni con due o più operatori. Per espressioni ancora più complesse, è possibile utilizzare un programma per generare le proposte di test.
Tabelle per espressioni booleane semplici
Se l'espressione è A&&B, testare con:
A
|
B
|
true
|
true
|
true
|
false
|
false
|
true
|
Se l'espressione è A||B, testare con:
A
|
B
|
true
|
false
|
false
|
true
|
false
|
false
|
Se l'espressione è A1 && A2 && ... &&
An, testare con:
A1, A2, ... e An sono tutti true
|
A1 è false, tutto il resto è true
|
A2 è false, tutto il resto è true
|
...
|
An è false, tutto il resto è true
|
Se l'espressione è A1 || A2 || ... || An, testare con:
A1, A2, ... e An sono tutti false
|
A1 è true, tutto il resto è false
|
A2 è true, tutto il resto è false
|
...
|
An è true, tutto il resto è false
|
Se l'espressione è A, testare con:
Quindi, quando è necessario testare a&&!b, è possibile applicare la prima tabella
descritta in precedenza, invertire il senso di b (perché è stato negato) e acquisire questo elenco di Proposte di test:
-
A true, B false
-
A true, B true
-
A false, B false
Espressioni relazionali
Ecco un altro esempio di codice con un errore:
if (finished < required) {
siren.sound();
}
< dovrebbe essere a <=. Tali errori sono abbastanza comuni. Come
avviene con le espressioni booleane, è possibile costruire una tabella di valori di test e vedere quali rilevano
l'errore:
terminati
|
richiesti
|
Il codice come è stato scritto...
|
Il codice corretto avrebbe dovuto...
|
1
|
5
|
fa suonare la sirena
|
ha fatto suonare la sirena
|
5
|
5
|
non fa suonare la sirena
|
ha fatto suonare la sirena
|
5
|
1
|
non fa suonare la sirena
|
non ha fatto suonare la sirena
|
Più generalmente, l'errore può essere rilevato ogni qualvolta terminati=richiesti. Da analisi di
errori plausibili, è possibile ricavare tali regole per le proposte di test:
Se l'espressione è A<B o A>=B, testare con i seguenti elementi:
A=B
|
A leggermente inferiore a B
|
Se l'espressione è A>B o A<=B, testare con i seguenti elementi:
A=B
|
A leggermente superiore a B
|
Cosa significa "leggermente"? Se A e B sono numeri interi, A dovrebbe essere inferiore o superiore di un numero
rispetto a B. Se sono numeri a virgola mobile, a dovrebbe essere un numero abbastanza vicino a B. (Probabilmente non è
necessario che sia il numero a virgola mobile più vicino a B).
Regole per espressioni relazionali e booleane
combinate
La maggior parte degli operatori relazionali si verificano in espressioni booleane, come nel seguente esempio:
if (finished < required) {
siren.sound();
}
Le regole per le espressioni relazionali comporterebbero le seguenti proposte di test:
-
terminati è uguale a richiesti
-
terminati è leggermente inferiore a questa espressione:richiesti
Le regole per le espressioni booleane comporterebbero le seguenti proposte di test:
-
terminati < richiesti dovrebbe essere true
-
terminati < richiesti dovrebbe essere false
E' possibile che questa espressione: terminati è leggermente inferiore a questa
espressione:richiesti, terminati < richiesti sia true, quindi non vi è
ragione di scrivere l'ultima.
Se questa espressione è false non vi è ragione di scrivere: terminati è uguale a richiesti, , terminati < richiesti .
Se un'espressione relazionale non contiene alcun operatore booleano (&& e ||), ignorare il fatto che è anche un'espressione booleana.
La faccenda si complica leggermente con combinazioni di operatori booleani e relazionali, come la seguente:
if (count<5 || always) {
siren.sound();
}
Dall'espressione relazionale, si ricava:
-
conteggio leggermente inferiore a 5
-
conteggio uguale a 5
Dall'espressione booleana, si ricava:
-
conteggio<5 true, sempre false
-
conteggio<5 false, sempre true
-
conteggio<5 false, sempre false
Tali espressioni possono essere combinate in tre ulteriori proposte di test più specifiche. (In questo caso, si noti
che conteggio è un numero intero).
-
conteggio=4, sempre false
-
conteggio=5, sempre true
-
conteggio=5, sempre false
Tenere presente che conteggio=5 viene utilizzato due volte. Potrebbe essere meglio utilizzarlo
una sola volta, per consentire l'utilizzo di un altro valore, dopo tutto, che senso ha testare questa espressione con
due 5: conteggio ? Non sarebbe meglio utilizzarla una sola volta con 5 e un'altra con un valore
differente in modo che il risultato sia false? (Esempio: conteggio<5 Potrebbe esserlo, ma è
rischioso provare. Il motivo è che è semplice commettere un errore. Si supponga di avere provato quanto segue:
-
conteggio=4, sempre false
-
conteggio=5, sempre true
-
conteggio<5 false, sempre false
Si supponga che è presente un errore che è possibile rilevare soltanto con il seguente valore: conteggio=5. Ciò significa che il valore 5 produrrà "false" nell'espressione conteggio<5, quando il codice corretto avrebbe prodotto true. Tuttavia, il valore false viene
immediatamente fatto seguire dal valore "sempre", che è true. Ciò significa che il valore dell'intera espressione è
corretto, anche se il valore dell'espressione secondaria relazionale è sbagliato. L'errore non verrà rilevato.
L'errore viene rilevato se è l'altro conteggio=5 che rimane meno specifico.
Problemi simili si verificano quando l'espressione relazionale si trova a destra dell'operatore booleano.
Dal momento che è difficile sapere quali espressioni secondarie devono essere esatte e quali possono essere generali, è
meglio renderle tutte esatte. L'alternativa è utilizzare il programma di espressioni booleane citato in precedenza. Esso produce proposte di test corrette per
espressioni relazionali e booleane combinate arbitrarie.
Proposte di test senza codice
Come spiegato in Concetto:
Test-first Design, è generalmente preferibile progettare i test prima di implementare il codice. Quindi, sebbene le
tecniche siano motivate da esempi di codice, verranno generalmente applicate senza codice. In che modo?
Alcuni artefatti di progettazione, ad esempio i grafici di stato e i diagrammi di sequenza, utilizzano espressioni
booleane come guardie. Questi casi sono semplici: aggiungono semplicemente le proposte di test dalle espressioni
booleane all'elenco di controllo delle proposte di test dell'artefatto. Consultare Linea guida del prodotto di lavoro: Proposte di test per il grafico di stato e per i
diagrammi di attività.
Il caso più complesso si verifica quando le espressioni booleane sono implicite anziché piuttosto esplicite. Ciò
avviene spesso nelle descrizioni delle API. Di seguito ne viene riportato un esempio. Considerare questo metodo:
List matchList(Directory d1, Directory d1,
FilenameFilter excluder);
La descrizione della funzionalità del metodo può essere letta in questo modo:
Restituisce un elenco dei nomi percorso assoluti di tutti i file visualizzati in entrambe le directory. Le
sottodirectory vengono generate. [...] I nomi file che corrispondono a excluder vengono
esclusi dall'elenco restituito. L'excluder si applica soltanto alle directory di livello più elevato, non ai nomi
file nelle sottodirectory.
Le parole "e", "o" non compaiono. Ma quando viene incluso un nome file nell'elenco di restituzione? Quando compare
nella prima directory e nella seconda directory e si trova in una directory di livello inferiore o
non viene escluso in modo specifico. Nel codice:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
Ecco alcune proposte di test per tale espressione, fornite in formato tabulare:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
false
|
false
|
false
|
true
|
false
|
false
|
L'approccio generale per la rilevazione di espressioni booleane implicite dal testo è, innanzitutto, elencare le
azioni descritte (ad esempio "restituisce un nome corrispondente"). Quindi scrivere un'espressione booleana che
descriva i casi in cui viene eseguita un'azione. Far derivare le proposte di test da tutte le espressioni.
Vi è anche spazio per un eventuale disaccordo in tale processo. Ad esempio, è possibile che una persona trascriva
l'espressione booleana utilizzata in precedenza. Un altro potrebbe affermare che, in effetti, sono presenti due azioni
distinte: prima il programma rileva i nomi corrispondenti e poi li filtra. Quindi, anziché un'espressione, ne sono
presenti due:
-
rileva corrispondenza:
-
si verifica quando un file si trova nella prima directory e un file con lo stesso nome si trova nella
seconda directory
-
filtra corrispondenza:
-
si verifica quando i file corrispondenti si trovano nel livello superiore e il nome corrisponde all'excluder
Questi differenti approcci possono generare proposte di test differenti e, quindi, test differenti. Ma le differenze
non sono poi così importanti. Ciò significa che il tempo impiegato nel cercare di capire quale espressione sia corretta
e nel tentare le alternative, potrebbe essere speso meglio su altre tecniche e per produrre ulteriori test. Per
ulteriori informazioni sulle varie differenze che potrebbero esistere, proseguire con la lettura.
La seconda persona penserà due serie di proposte di test.
proposte di test sulla rilevazione di una corrispondenza:
-
file nella prima directory, file nella seconda directory (true, true)
-
file nella prima directory, file non presente nella seconda directory (true, false)
-
file non presente nella prima directory, file nella seconda directory (false, true)
proposte di test sul filtraggio di una corrispondenza (una volta che è stata rilevata):
-
i file corrispondenti si trovano nel livello superiore, il nome corrisponde all'excluder
(true, true)
-
i file corrispondenti si trovano nel livello superiore, il nome non corrisponde all'excluder (true, false)
-
i file corrispondenti si trovano in un livello inferiore, il nome corrisponde all'excluder (false, true)
Si suppongano due serie di proposte di test combinate. Quelle nella seconda serie sono rilevanti solo quando il file è
presente in entrambe le directory, quindi tali proposte possono essere combinate solo con la prima proposta nella prima
serie. Tale situazione genera quanto segue:
file nella prima directory
|
file nella seconda directory
|
livello superiore
|
corrispondenza con l'excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
Due delle proposte di test relative alla rilevazione di una corrispondenza non compaiono in tale tabella. E' possibile
aggiungerle nel seguente modo:
file nella prima directory
|
file nella seconda directory
|
livello superiore
|
corrispondenza con l'excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
Le celle vuote indicano che le colonne sono irrilevanti.
Questa tabella è ora abbastanza simile alla tabella della prima persona. La somiglianza può essere enfatizzata
utilizzando la stessa terminologia. La tabella della prima persona contiene una colonna denominata "inLower" e quella
della seconda persona ne ha una denominata "nel livello più elevato". Tali colonne possono essere convertite
modificando leggermente il senso dei valori. In tal modo, si ottiene questa versione della seconda tabella:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
Le prime tre righe sono identiche alla tabella della prima persona. Le ultime due differiscono solo nel fatto che
questa versione non specifica i valori specificati dalla prima. Ciò deriva da una presupposizione relativa al modo in
cui il codice è stato scritto. La prima ha presupposto un'espressione booleana complicata:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
La seconda presuppone espressioni booleane nidificate.
if (appearsInFirst && appearsInSecond) {
// rilevata una corrispondenza.
if (inTopLevel && excluded) {
// filtrarla
}
}
la differenza tra le due è che le proposte di test per la prima rilevano due errori ignorati dalle proposte della
seconda, perché tali errori non si applicano.
-
Nella prima implementazione, è possibile che sia presente un errore tra parentesi. Le parentesi intorno a || sono corrette o sbagliate? Dal momento che la seconda implementazione non prevede alcuna
parentesi e nessun ||, l'errore non può esistere.
-
I requisiti del test per la prima implementazione verificano se la seconda espressione, &&, dovrebbe essere ||. Nella seconda implementazione, tale && esplicito è sostituito dall'implicito &&. Non vi è alcun
errore ||-per-&& . (E' possibile che la nidificazione non sia
corretta, ma tale tecnica non lo verifica).
|