Verifica e validazione: analisi dinamica
Definizione e problemi
L'analisi dinamica non è altro che l'esecuzione di test, cioè prove su del codice in esecuzione. Purtroppo nello sviluppo di un software il codice nasce molto tardi; per questo, il testing dev'essere rapido (efficiente) ed efficace[1]. Un altro problema dei test è la loro non esaustività: è possibile eseguirli solo su un insieme finito di casi, che non sono quasi mai tutti i casi possibili.
La pianificazione del testing deve quindi avvenire il prima possibile. Il primo momento utile è la progettazione del sistema.
Il debugging non è verifica: esso nasce da un errore che si è manifestato inaspettatamente, mentre la verifica viene pianificata a monte dello sviluppo.
Terminologia
Compito del test è trovare errori nel codice. Distinguiamo tre livelli di errore:
- Malfunzionamento (failure): l'esecuzione è difforme dalle attese. I malfunzionamenti riguardano il comportamento del software.
- Errore (error): stato del sistema che, se attivato, produce un malfunzionamento; se l'errore non viene attivato, rimane nascosto ma esiste. Gli errori sono quindi stati del sistema.
- Guasto o difetto (fault): causa dell'errore; può essere un guasto del computer o del software. I guasti possono essere malfunzionamenti di un sotto-sistema: i sistemi software sono sistemi gerarchici (annidati).
Quindi: un guasto cause un errore, il quale può produrre un malfunzionamento. (Nota: il glossario IEEE non concorda pienamente con le definizioni sopra.)
Organizziamo il testing nelle seguenti classi:
- caso di prova (test case) — una tripla <ingresso, uscita, ambiente>;
- batteria di prove (test suite) — un insieme di casi di prova;
- prova — una procedura di prova e una batteria di prove.
Compromesso
Esiste un compromesso, come tra efficienza ed efficacia, tra il numero di test sufficienti a verificare il prodotto e lo sforzo allocato a progetto. Sul testing vale infatti la "legge del rendimento decrescente[2]": man mano che aumento lo sforzo, il rendimento cresce inizialmente ma poi diminuisce sempre più. Questo avviene, ad esempio, quando un produttore aumenta la produzione e, a un certo punto, oltrepassa la domanda: i profitti iniziano ad essere negativi. Così, arriva un tempo in cui fare altri test non aggiunge nulla, cioè non trova errori (e la funzione primaria dei test è trovare errori).
Criteri guida per i test
Oggetto di una prova può essere:
- il sistema nel suo complesso (per i test di sistema);
- parti del sistema in relazione funzionale, d'uso, di comportamento o di struttura (per i test di integrazione);
- singole unità (per i test di unità).
L'obiettivo di ogni prova dev'essere specificato in modo chiaro per ogni caso di prova (test case): un test è buono se è ripetibile. Per essere ripetibile, ogni test deve stare attento anche allo stato, all'ambiente. Il Piano di Qualifica specifica quali e quante sono le prove da effettuare. I test non sostituiscono la progettazione; piuttosto, sono speculari ad essa.
Un test deve cercare di far fallire il software: dev'essere "cinico"! I test che falliscono devono essere eseguiti sempre, dal momento in cui sono falliti e il software è stato corretto: Any failed execution must yield a test case, to be permanently included in the project's test suite
(Bertrand Meyer).
All'origine, un test va specificato in fase di progettazione; dopo essere stato implementato ed eseguito, esso va tenuto in un archivio, come documentazione dell'attività di testing.
Test di unità
I test di unità sono naturalmente più numerosi dei test di integrazione e di sistema: circa due terzi dei difetti rilevati tramite analisi dinamica sono dovuti ai test di unità.
Un concetto fondamentale nei test di unità è quello di copertura (coverage). Con questo termine si intende la percentuale di codice che un caso di prova è in grado di eseguire, cioè quanto codice sorgente è stato effettivamente attraversato durante il caso di prova; ad esempio, una copertura del 100% indica che l'esecuzione di un test ha coperto tutti i casi possibili del codice in esame.
Alcuni criteri notevoli di copertura sono i seguenti:
- function coverage — quante funzioni (sottoprocedure) sono state eseguite?
- statement coverage — quante istruzioni (righe di codice, all'incirca) sono state eseguite?
- branch coverage — quanti rami[3] del programma sono stati eseguiti?
- condition coverage — quante espressioni logiche hanno assunto entrambi i valori possibili?
Di queste, le più importanti sono lo statement coverage e, ancor più, il branch coverage. È sempre bene che i test di unità coprano il codice al 100% rispetto a questi ultimi due criteri; tuttavia, va ricordato che la copertura totale del codice non assicura l'assenza di difetti! Un criterio ancora più forte del branch coverage è il MC/DC (Modified Condition/Decision Coverage).
Distinguiamo due categorie di test di unità:
- Un test funzionale è un test a scatola chiusa (black box). Fa riferimento alla specifica di un'unità e osserva il suo comportamento dal di fuori; dati in ingresso che producono un medesimo comportamento funzionale costituiscono una classe di equivalenza e sono un caso di prova.
- Un test strutturale è un test a scatola aperta (white box): verifica la logica interna del codice dell'unità. Persegue la massima copertura[4] del codice sorgente; dati in ingresso che attivano un medesimo percorso costituiscono un caso di prova.
Dobbiamo eseguire i test funzionali prima di quelli strutturali, se non vogliamo rischiare di analizzare una struttura che non svolge il compito giusto. I test funzionali vanno sempre integrati con test strutturali.
Test di integrazione
I test d'integrazione fanno parte di un processo più ampio che è quello dell'integrazione delle parti del sistema. Le parti vanno integrate secondo una strategia. Ad esempio è sempre bene assemblare le parti in modi incrementale (quindi reversibile), seguendo le dipendenze nell'architettura: aggiungendo una parte nuova ad un insieme ben verificato, i difetti rilevati in un test d'integrazione saranno probabilmente dovuti alla parte nuova, facilitando così la ricerca di quale parte sia da correggere.
Basandoci sul fatto che i sistemi software sono (al giorno d'oggi) sistemi gerarchici, possiamo individuare due principali strategie d'integrazione:
- Dal basso (bottom-up): si sviluppano e si integrano prima le parti con minore dipendenza funzionale (fan-out) e maggiore utilità (fan-in). Così facendo, si "risale" l'albero delle dipendenze. Si economizzano molti stub ma le funzionalità di alto livello compaiono più tardi.
- Dall'alto (top-down): si sviluppano prima le parti più esterne (di alto livello, con molte dipendenze) e poi si scende nell'albero delle dipendenze. Qui le funzionalità di alto livello vengono verificate sin da subito e si può mostrare al committente una bozza del sistema.
I test d'integrazione si applicano alle componenti specificate in progettazione architetturale; perciò, questi test rilevano difetti di progettazione. L'integrazione delle componenti costituisce il sistema completo.
Quanti test d'integrazione è bene fare? tanti quante sono le interfacce nell'architettura del sistema: i test d'integrazione devono accertare che i dati scambiati attraverso ciascuna interfaccia siano conformi alla propria specifica.
Test di sistema
I test di sistema verificano il comportamento del sistema rispetto ai suoi requisiti. Essi sono inerentemente funzionali: non hanno bisogno di conoscere la logica interna del sistema.