Category partition

La tecnica di category partition è un metodologia che permette di caratterizzare e identificare le classi di equivalenza del dominio di un problema a partire dalle sue specifiche. Può essere utilizzata a vari livelli a seconda che si debbano realizzare test di unità, test di integrazione e o test funzionali.

Il metodo è composto da una serie di passi in sequenza:

  1. analisi delle specifiche: in questa fase vengono identificate le unità funzionali individuali che possono essere verificate singolarmente; non necessariamente sono un’unica classe, è sufficiente che siano componenti facilmente separabili dal resto, sia a livello di testing che concettuale. Per ogni unità vengono quindi identificate delle caratteristiche (categorie) dei parametri e dell’ambiente in cui opera;
  2. scegliere dei valori: per ogni categoria, occorre scegliere quali sono i valori sensati su cui fare riferimento;
  3. determinare eventuali vincoli tra le scelte, che non sono sempre indipendenti;
  4. scrivere test e documentazione.

Per capire meglio ciascuna di tali fasi vediamo un’esempio di utilizzo della tecnica di category partition prendendo come soggetto il comando find della shell Linux.

PASSO 1 – analizzare le specifiche

Per prima cosa analizziamo le specifiche del comando:

Syntax: find <pattern> <file>

The find command is used to locate one or more instances of a given pattern in a file. All lines in the file that contain the pattern are written to standard output. A line containing the pattern is written only once, regardless of the number of times the pattern occur in it.

The pattern is any sequence of characters whose length does not exceed the maximum length of a line in the file. To include a blank in the pattern, the entire pattern must be enclosed in quotes ("). To include a quotation mark in the pattern, two quotes ("") in a row must be used.

Vista la relativa semplicità, find è un’unità funzionale individuale che può essere verificata separatamente. Bisogna dunque individuarne i parametri: come è chiaro dalla sintassi essi sono due, il pattern da cercare e il file in cui farlo.

Ora, ciascuno di tali parametri può possedere determinate caratteristiche, ed è nostro compito in questa fase comprenderle ed estrarle.
Tali caratteristiche possono essere di due tipi: esplicite, ovvero quelle ricavabili direttamente dalla lettura specifiche, e implicite, ovvero quelle che provengono dalla nostra conoscenza del dominio di applicazione e che quindi non vengono specificate.

Tornando al nostro caso di studio possiamo per esempio ottenere la seguente tabella:

Oggetto Caratteristiche esplicite Caratteristiche implicite

pattern

  • lunghezza del pattern;
  • pattern tra doppi apici;
  • pattern contenente spazi;
  • pattern contenente apici.
  • pattern tra apici con/senza spazi;
  • più apici successivi inclusi nel pattern.

file
(nome)

(nessuna)
  • caratteri nel nome ammissibili o meno;
  • file esistente (con permessi di lettura) o meno.

file
(contenuto)

  • numero occorrenze del pattern nel file;
  • massimo numero di occorrenze del pattern in una linea;
  • massima lunghezza linea.
  • pattern sovrapposti;
  • tipo del file.

È importante esplicitare le caratteristiche implicite dei parametri dell’unità funzionale perché le specifiche non sono mai complete e solo così possiamo disporre di tutti gli elementi su cui ragionare nelle fasi successive.

Si presti poi attenzione alla distinzione fatta tra il nome del file e il suo contenuto: il primo infatti è un parametro che viene passato al comando per iniziarne l’esecuzione, mentre il secondo fa parte dell’ambiente in cui il comando opera ed è dunque soggetto ad una sottile distinzione concettuale.

ALPHA E BETA TESTING

Spesso, però, analizzare le specifiche non basta per comprendere tutte le variabili che entrano in gioco durante l’esecuzione di un programma. Bisogna infatti ricordare che ci sono moltissime altre caratteristiche d’ambiente che ancora non sono state considerate: la versione del sistema operativo, del browser, il tipo di architettura della macchina su cui gira il programma eccetera.

Spesso, quindi, la fase di testing funzionale si divide in due fasi:

  • alpha testing: l’unità funzionale viene testata in-house, ovvero su una macchina all’interno dello studio di sviluppo. In questa fase si considerano soprattutto le caratteristiche legate alle specifiche di cui sopra;
  • beta testing: per testare varie configurazioni d’ambiente una versione preliminare del programma viene distribuito in un ambiente variegato per osservare come esso si comporta sulle macchine di diversi utenti.

Per il momento, però, consideriamo solo la fase di alpha testing e le categorie ad essa relative.

PASSO 2 – scegliere dei valori

Individuate le caratteristiche dei parametri e delle variabili d’ambiente da cui l’unità funzionale dipende, che prendono il nome di categorie, si passa quindi alla seconda fase.

In questa fase si devono identificati tutti e i soli casi significativi per ogni categoria, ovvero quei valori della stessa che si ritiene abbia senso testare; poiché si tratta di un compito molto soggettivo è importante in questa fase avere esperienza (know-how) nel dominio d’applicazione.

Per capire meglio di cosa stiamo parlando ritorniamo al nostro esempio e consideriamo il parametro pattern. Per ciascuna delle sue categorie possono essere individuati vari casi significativi:

  • lunghezza del pattern: vuoto, un solo carattere, più caratteri, più lungo di almeno una linea del file;
  • presenza di apici: pattern tra apici, pattern non tra apici, pattern tra apici errati;
  • presenza di spazi: nessuno spazio nel pattern, uno spazio nel pattern, molti spazi nel pattern;
  • presenza di apici interni: nessun apice nel pattern, un apice nel pattern, molti apici nel pattern.

È interessante notare il mantra già visto del “nessuno, uno, molti”, spesso molto utile in questa fase.

PASSO 3 – determinare i vincoli tra le scelte

Trovati tutti i valori significativi delle categorie dell’unità funzionale come possiamo costruire i casi di test da utilizzare per verificarne la correttezza?

Si potrebbe pensare di testare tutte le combinazioni di valori significativi, facendo cioè il prodotto cartesiano tra le categorie. Nella pratica, però, ciò risulterebbe in un numero esagerato di casi di test: già solo nel nostro semplice esempio questi sarebbero ben 1944, decisamente troppi.

Nel tentativo di evitare quest’esplosione combinatoria ci si accorge però che spesso le anomalie sorgono dall’interazione di coppie di caratteristiche indipendentemente dal valore assunto da tutte le altre: per esempio, un problema potrebbe presentarsi se si usa il browser Edge sul sistema operativo Linux, indipendentemente da caratteristiche quali la dimensione dello schermo, l’architettura del processore eccetera.
Per ridurre il numero di casi di test si sviluppa quindi la tecnica del pairwise testing, che riduce l’insieme delle configurazioni da testare a tutte le combinazioni di coppie di valori. È quindi presente almeno un caso di test per ogni coppia ipotizzabile di valori: in rete e in Java sono presenti diversi strumenti che permettono di creare casi di test combinati con il metodo pairwise.

Un’ulteriore tentativo di ridurre il numero di casi di test prevede di definire una serie di vincoli per la generazione delle coppie, escludendo particolari combinazioni di caratteristiche: così, per esempio si potrebbe escludere la coppia “OS == MacOs” e “browser == Edge” perché sfruttando la conoscenza di dominio sappiamo che tale browser non è disponibile sul suddetto sistema operativo.
Volendo essere più precisi, la creazione di vincoli prevede un passaggio intermedio: vengono definite una serie di proprietà (es. NotEmpty o Quoted per l’esempio su find) e si creano dei vincoli logici a partire da esse. I vincoli seguono poi una struttura tra le seguenti:

  • se: si può limitare l’uso di un valore solo ai casi in cui è definita una proprietà. Per esempio, è inutile testare il caso “il file non esiste” se la proprietà NotEmpty si manifesta;
  • single: alcune caratteristiche prese singolarmente anche se combinate con altre generano lo stesso risultato. Per esempio, se il file non contiene occorrenze del pattern cercato il risultato del programma è indipendente dal tipo di pattern cercato;
  • error: alcune caratteristiche generano semplicemente errore, come per esempio se si omette un parametro.

PASSO 4 – scrivere i test

Fissati i vincoli e fatti i calcoli combinatori si procede ad enumerare iterativamente tutti i casi di test generati continuando ad aggiungere vincoli fino ad arrivare ad un numero ragionevole.

Ovviamente, i casi di test avranno poi bisogno di valori specifici per le caratteristiche: non basta dire “pattern con apici all’interno”, bisogna creare un pattern aderente a questa descrizione! Fortunatamente questa operazione è solitamente molto facile, anche con tool automatici.

Conclusioni

Per quanto intuitiva e utile, la tecnica di category partition presenta due criticità:

  • individuare i casi significativi delle varie caratteristiche può essere difficile e si può sbagliare, anche utilizzando mantra come “zero, uno, molti”;
  • una volta generati i casi di test serve comunque un “oracolo” che fornisca la risposta giusta, ovvero quella che ci si attende dall’esecuzione sul caso di test. L’attività non è dunque completamente automatizzabile.

Va però detto che esistono delle tecniche di property-based testing che cercano di eliminare la necessità di un oracolo considerando particolari proprietà che dovrebbero sempre valere durante l’esecuzione (invarianti) piuttosto che analizzare il risultato dell’esecuzione dei casi di test per determinare la correttezza del programma.