Terminologia

Verifica e convalida

Verifica e convalida sono due termini con un significato apparentemente molto simile ma che celano in realtà una differenza non banale tra loro:

  • per verifica (della correttezza) si intende l’attività di confronto del software con le specifiche (formali) prodotte dall’analista;
  • per convalida (dell’affidabilità) si intende l’attività di confronto del software con i requisiti (informali) posti dal committente.

Ci sono quindi due punti critici che vanno a sottolineare maggiormente questa differenza:

  • requisiti e specifiche sono spesso formulati diversamente. Solitamente i requisiti, essendo scritti dal committente, sono formulati in un linguaggio più vicino al dominio di quest’ultimo. Diversamente, le specifiche sono scritte in un linguaggio più vicino al dominio dello sviluppatore, spesso in maniera formale e poco ambigua;
  • è facile che i requisiti cambino in corso d’opera mentre le specifiche rimangano congelate; questo aspetto dipende molto dai contratti tra committente e il team di sviluppo.

La definizione dei requisiti forniti dal cliente è immediata ma informale: scrivere dei test che li convalidano può risultare molto complicato. Invece, è più semplice validare le specifiche attraverso test in quanto sono scritte dal team di sviluppo e sono quindi più formali e complete.

Ad ogni modo, nelle attività di verifica e convalida si cercano degli errori, ma la parola “errore” stessa può assumere molti significati a seconda del contesto. È quindi importante capire di quale errore si sta parlando, introducendo termini diversi, come malfunzionamento e difetto.

N.B: Esistono dei glossari e vocabolari di terminologia comune redatti dalla IEEE, ad esempio Systems and software engineering — Vocabulary che possono essere addottati dagli sviluppatori come standard in modo da snellire la comunicazione tra di loro.

Malfunzionamento (guasto/failure)

Un malfunzionamento è uno scostamento dal corretto funzionamento del programma.

Non dipende dal codice e in generale non ci si accorge di esso osservando il codice ma solo da un punto di vista più esterno, utilizzando il programma. Il malfunzionamento potrebbe riguardare sia le specifiche (quindi relativo alla fase di verifica) che i requisiti (fase di convalida, ovvero “non rispetta le aspettative”). Secondo il vocabolario citato in precedenza:

failure:

  1. termination of the ability of a product to perform a required function or its inability to perform within previously specified limits. ISO/IEC 25000:2005, Software Engineering — Software product Quality Requirements and Evaluation (SQuaRE) — Guide to SQuaRE.4.20.
  2. an event in which a system or system component does not perform a required function within specified limits.

NOTE: A failure may be produced when a fault is encountered

Esempio

Di seguito è illustrato un esempio di malfunzionamento.

static int raddoppia(int par) {
    int risultato;
    risultato = (par * par);
    return risultato;
}

static void main(String[] args) {
    int risultato = raddoppia(3);
    System.out.println(risultato);  // 9
}

La funzione dovrebbe ritornare il doppio del numero in ingresso, ma se passiamo 3 in argomento verrà ritornato 9.

Difetto (anomalia/fault)

Un difetto è il punto del codice che causa il verificarsi del malfunzionamento.

È condizione necessaria (ma non sufficiente) per la verifica di un malfunzionamento.

fault:

  1. a manifestation of an error in software.
  2. an incorrect step, process, or data definition in a computer program.
  3. a defect in a hardware device or component. Syn: bug

NOTE: A fault, if encountered, may cause a failure.

Nell’esempio di codice precedente, il difetto è in risulato = (par * par).

Il difetto è condizione non sufficiente per il verificarsi di un malfunzionamento: ad esempio, non si verificano malfunzionamenti in caso l’argomento passato sia 0 oppure 2. Il raddoppio in quei casi avverrebbe in maniera corretta.

Un altro esempio di tale è proprietà è il caso in cui esistono “più anomalie che si compensano”: se si sta utilizzando una libreria per operazioni su temperature in gradi Fahrenheit, ponendo il caso che stia partendo da gradi Celsius, dovrà essere effettuata una conversione. Se in questa conversione è presente un’anomalia che però si riflette allo stesso modo in fase di riconversione per restituire il risultato, le due anomalie combinate non si manifestano in un malfunzionamento.

Spesso le anomalie si annidano nella gestione di casi particolari o eccezionali del programma in quanto il flusso di controllo ordinario è solitamente il più testato.

Sbaglio (mistake)

Uno sbaglio è la causa di un’anomalia. Si tratta in genere di un errore umano.

mistake:

  1. a human action that produces an incorrect result

NOTE: The fault tolerance discipline distinguishes between a human action (a mistake), its manifestation (a hardware or software fault), the result of the fault (a failure), and the amount by which the result is incorrect (the error).

Relativamente all’esempio precedente, possibili sbagli possono essere:

  • errori di battitura (scrivere * invece di +);
  • concettuali (non sapere cosa vuol dire raddoppiare);
  • relativi alla padronanza del linguaggio (credere che * sia il simbolo dell’addizione).

È importante capire quale sia la causa di uno sbaglio in modo da poter intraprendere azioni correttive per il futuro (es. studiare meglio la sintassi del linguaggio).

Esempio notevole: il caso Ariane 5

Wikipedia: Ariane 5 notable launches

Il 4 giugno 1996 il primo volo di prova del razzo Ariane 5 è fallito a causa di un problema al software di controllo che ha portato all’autodistruzione del missile.

Il malfunziamento è palese: il razzo è esploso e chiaramente non era il comportamento richiesto.

Qual era l’anomalia? Il malfunziamento si è verificato per una eccezione di overflow, sollevatosi durante una conversione da un 64 bit float a un 16 bit signed int che indicava il valore della velocità orizzontale. Questo ha bloccato sia l’unità principale che il backup dell’unità stessa.

Lo sbaglio? Tale eccezione non veniva gestita perché questa parte del software era stata ereditata da Ariane 4, modello di razzo antecedente a Ariane 5, la cui traiettora faceva sì che non si raggiungessero mai velocità orizzontali non rappresentabili con int 16 bit. La variabile incriminata non veniva protetta per gli “ampi margini di sicurezza” (a posteriori, non così ampi).

Il comportamento della variabile non era mai stato analizzato con i dati relativi alla traiettoria da seguire.

Tecniche di verifica e convalida

Classificazione delle tecniche

Nell’ambito della verifica e convalida è possibile classificare le tecniche di analisi in due categorie:

  • tecniche statiche, basate sull’analisi degli elementi sintattici del codice.
    Ad esempio: metodi formali, analisi del dataflow e modelli statistici;
  • tecniche dinamiche, basate sull’esecuzione del programma.
    Ad esempio: testing e debugging.

In generale, è più facile determinare tecniche dinamiche rispetto alle tecniche statiche. Per contro, una volta ideate e a patto di avere dimensioni del codice ragionevoli e costrutti sintattici non troppo complessi le tecniche statiche sono più veloci nell’analizzare il codice e, soprattutto, più complete dato che le tecniche dinamiche lavorano sui possibili stati del programma - che possono essere infiniti.

Ovviamente diverse metodologie di verifica e convalida avranno i rispettivi pro e contro. Come si possono dunque confrontare queste tecniche?

Nell’immagine sopra è possibile osservare una piramide immaginaria a 3 dimensioni che riassume dove si posizionano le tecniche di verifica e convalida relativamente le une con le altre. La cima della piramide rappresenta il punto ideale a cui tendere, nel quale è possibile affermare di esser riusciti a verificare perfettamente una proprietà arbitraria attraverso una prova logica (dal lato statico) o una ricerca esaustiva su tutti gli stati del problema (dal lato dinamico).

Tale punto ideale è praticamente impossibile da raggiungere per la stragrande maggioranza dei problemi che siamo interessati a risolvere. Bisogna scegliere da quale versante iniziare la scalata della piramide: lato verde (approccio statico) o lato blu (approccio dinamico)?

Più ci si posiziona verso il basso, più si degenera in:

  • estrema semplificazione delle proprietà (in basso a sinistra): si stanno in qualche modo rilassando eccessivamente gli obiettivi che si vogliono raggiungere.

    Ad esempio, se si vuole dimostrare che si sta usando un puntatore in maniera corretta e nel farlo si sta semplicemente controllando che non valga null, è cambiata la proprietà che si vuole come obiettivo (controllare che un puntatore non valga null non significa che lo si stia usando nel modo corretto);

  • estrema inaccuratezza pessimistica (in basso al centro): è dovuta all’approccio pessimistico che ha come mantra:

    “Se non riesco a dimostrare l’assenza di un problema assumo che il problema sia presente”

    Ad esempio, si manifesta nei compilatori quando non riescono a dimostrare che una determinata funzione che deve ritornare un valore ritorni effettivamente un valore per tutti i possibili cammini if / else if / eccetera. La mancanza di capacità nel dimostrare l’assenza di un problema non ne implica la presenza di uno.

  • estrema inaccuratezza ottimistica (in basso a destra): è dovuta all’approccio ottimistico che ha come mantra:

    “Se non riesco a dimostrare la presenza di un problema assumo che questo non sia presente”

    È una possibile deriva degli approcci legati al testing: con esso si cercano malfunzionamenti, se a seguito dei test non ne vengono trovati allora si assume che il programma funzioni correttamente.

A metà strada tra questi estremi inferiori e l’estremo superiore ideale si posizionano quindi le varie tecniche di verifica e convalida, ciascuna più o meno legata ai tra approcci sopra descritti. Tra queste evidenziamo le dimostrazioni con metodi formali, il testing e il debugging.

Metodi formali

L’approccio dei metodi formali tenta di dimostrare l’assenza di anomalie nel prodotto finale.
Si possono utilizzare diverse tecniche (spiegate nelle lezioni successive), come:

  • analisi di dataflow;
  • dimostrazione di correttezza delle specifiche logiche.

Questo approccio segue la linea dell’inaccuratezza pessimistica.

Testing

Il testing è l’insieme delle tecniche che si prefiggono di rilevare malfunzionamenti.
Attraverso il testing non si può dimostrare la correttezza ma solo aumentare la fiducia dei clienti rispetto all’affidabilità del prodotto.

Le tecniche di testing possono essere molto varie e si raggruppano in:

  • white box: si ha accesso al codice da testare e si possono cercare anomalie guardandolo da un punto di vista interno;
  • black box: non si ha accesso al codice ma è possibile testare e cercare malfunzionamenti tramite le interfacce esterne;
  • gray box: non si ha accesso al codice ma si ha solo un’idea dell’implementazione ad alto livello.
    Per esempio, se sappiamo che il sistema segue il pattern MODEL VIEW CONTROLLER ci si può aspettare che certe stimolazioni portino a chiamate al database mentre altre no.

Come è chiaro, questo approccio segue una logica di inaccuratezza ottimistica.

È inoltre interessante notare che il Test Driven Development (TDD) adotta una filosofia di testing completamente black box: imponendo che venga scritto prima il test del codice questo non può assumere niente sul funzionamento interno dell’oggetto di testing.

Debugging

Dato un programma e un malfunzionamento noto e riproducibile, il debugging permette di localizzare le anomalie che causano i malfunzionamenti. A differenza del testing, infatti, è richiesta la conoscenza a priori di un malfunzionamento prima di procedere con il debugging.

Molto spesso viene usato il debugging al posto del testing, almeno a livello di terminologia: questo è un problema perché il debugging non è fatto per la “grande esecuzione” ma al contrario per esaminare in maniera granulare (a volte anche passo passo per istruzioni macchina) una determinata sezione di codice in esecuzione con lo scopo di trovare l’anomalia che provoca un malfunzionamento. Se si usassero le tecniche di debugging per effettuare il testing il tempo speso sarebbe enorme: il debugging osserva infatti stati interni dell’esecuzione e per rilevare un malfunzionamento in questo modo sarebbe necessario osservare tutti i possibili - e potenzialmente infiniti - stati del programma.

Due possibili approcci al debugging sono:

  • partendo da una malfunzionamento noto e riproducibile si avvia una procedura di analisi basata sulla produzione degli stati intermedi dell’esecuzione del programma: passo passo (a livello a piacere, da istruzione macchina a chiamata di funzione) si controllano tutti gli stati di memoria alla ricerca di uno inconsistente;
  • divide-et-impera: il codice viene smontato sezione per sezione e componente per componente in modo da poter trovare il punto in cui c’è l’anomalia. Si possono mettere breakpoint o “print tattiche”.