I bachi (bug)

Il termine bug (insetto) è usato comunemente per indicare gli errori nei programmi.

Leggenda vuole che sia stato adottato nel gergo informatico all'epoca dei computer elettromeccanici in occasione del ritrovamento di un insetto (una falena) intrappolata dentro un interruttore a relè (1947, nel Mark II della Harvard University).

Il primo bug catalogato

In realtà l'uso del termine per indicare un insetto immaginario nascosto in un apparecchio e responsabile di tutti i malfunzionamenti era probabilmente già in auge dal secolo precedente.

I bachi di cui ci occuperemo non sono davvero insetti ma neppure immaginari.




Le conseguenze degli errori

Come molte altre attività umane (e non), la scrittura di programmi non è esente da errori di varia natura che ne possono compromettere il corretto funzionamento.

Con la sempre maggiore diffusione che le applicazioni software hanno nelle nostre attività quotidiane (es. cellulari, bancomat, mezzi di trasporto, apparecchiature mediche) il verificarsi di errori potrebbe influenzare pesantemente le nostre esistenze.

Un caso tristemente emblematico è quello del THERAC-25, un dispositivo computerizzato per la terapia a emissione di radiazioni destinata ai malati di cancro. A causa di un errore nel software di controllo, alcune di queste macchine rilasciarono dosi eccessive di radiazioni uccidendo alcuni pazienti e menomandone altri (negli anni 1985-1987). Secondo la versione ufficiale:
  • il programma fu scritto da un singolo programmatore non più rintracciabile e le cui qualifiche e livello di studi non furono mai accertati
  • l'elenco di specifiche non fu ritrovato
  • il programma era scarsamente documentato
  • il piano di collaudo non esisteva
  • gli errori erano dovuti a una soluzione ad hoc (dilettantistica) delle attività multitasking (controlli della tastiera, schermo, stampante e rilascio di radiazioni)
  • anche il software del modello precedente (THERAC-20) conteneva gli stessi errori, ma era dotato di blocchi hardware che impedivano meccanicamente il rilascio di radiazioni in dosi eccessive (nel THERAC-25 erano stati sostituiti da controlli software)




Esistono programmi esenti da errori?

La domanda che sorge spontanea anche ad un profano dell'informatica è quindi:

Come garantire che un sistema software sia esente da errori?

Purtroppo la risposta è che nessuno può darci tale garanzia assoluta... però è nostro dovere cercare di acquisire la maggiore fiducia possibile nel fatto che il programma che abbiamo scritto sia corretto, al punto da essere disposti ad affidare al programma persino la nostra incolumità e quella degli altri.

Anche con la certezza matematica che un certo programma sia privo di errori, il suo funzionamento può dipendere da altri fattori, quali la correttezza del compilatore, dell'interprete, del sistema operativo, dei driver di alcuni dispositivi, dei componenti hardware... senza contare le condizioni climatiche e ambientali della macchina su cui il programma viene eseguito!

Tutti fattori sui quali il programmatore ha scarso controllo.

Tralasciando l'impiego di metodi formali (che non può essere oggetto di questo corso), le migliori armi a nostra disposizione per eliminare quanti più errori possibili sono:

  • Il collaudo (testing): serve per identificare i malfunzionamenti del programma.
  • Il debug: serve per localizzare ed eliminare i difetti che provocano i malfunzionamenti.

Nota: difetti e malfunzionamenti non sono sempre in corrispondenza biunivoca: (1) l'esecuzione di un programma difettoso può produrre comunque risultati corretti, (2) esistono malfunzionamenti causati da combinazioni di più difetti e dipendenti dal particolare contesto di esecuzione.




Il collaudo di unità

Qualsiasi applicazione composta da più classi e metodi può essere controllata più facilmente utilizzando (anche in fase di sviluppo) il collaudo delle singole unità separatamente (al livello di granularità del singolo metodo o di gruppi ristretti di metodi cooperanti).

Questa pratica è diffusa anche nella progettazione di schede elettroniche, dove infatti troverete dei punti di connessione che non servono direttamente per il funzionamento dell'apparecchiatura, ma che sono stati predisposti affinché il personale delle riparazioni possa collegarci strumenti di misurazione o altre schede, al fine di individuare il guasto.

Il collaudo procede compilando ciascuna classe assieme a un semplice programma detto infrastruttura di collaudo che invoca i metodi da collaudare con opportuni valori e ne osserva l'esito.

È buona pratica eseguire il collaudo in maniera preventiva, prima di aver terminato la realizzazione dell'intera applicazione, per poter intervenire tempestivamente anche sulla riprogettazione del sistema in seguito alla scoperta di errori difficili da gestire adeguatamente.




Copertura del collaudo

Gli argomenti per il collaudo possono provenire da fonti diverse:
  • un insieme di valori prefissato
  • un insieme di valori inseriti dall'utente
  • un insieme di valori generato casualmente
  • un insieme di valori letto da file

L'uso di valori memorizzati in un file è preferibile perché copre gli altri casi (es. il contenuto del file può essere generato in maniera pseudocasuale) e rende più facile e veloce ripetere l'esperimento dopo aver modificato e ricompilato il codice.

Anche se il programma è predisposto per leggere i dati in input da tastiera, possiamo usare l'opzione di redirezione dello standard input per fornire dati memorizzati in un file al momento in cui si esegue il test:

   java classeCollaudo < fileDatiInput

I valori utilizzati per il collaudo dovrebbero coprire le seguenti categorie:

  • casi di prova positivi (valori di ingresso validi per i quali ci si aspetta una corretta gestione da parte del programma)

    Il collaudo dovrebbe verificare che l'elaborazione di questi valori dia sempre risultati corretti.

  • casi limite (valori di ingresso considerati validi ma inusuali, sono quelli che comunemente causano errori dovuti a trascuratezza del programmatore, come divisioni per zero, accesso a posizioni inesistenti negli array vuoti e nelle stringhe vuote, riferimenti null)

    Il collaudo dovrebbe verificare che l'elaborazione di questi valori dia risultati corretti senza causare eccezioni.

  • casi di prova negativi (valori di ingresso non validi che dovrebbero essere rifiutati dal programma)

    Il collaudo dovrebbe verificare che l'elaborazione di questi valori sollevi le opportune eccezioni (senza causare la terminazione del programma di collaudo!).




I pacchetti di prova

Nei casi di prova, oltre ai dati in ingresso è indispensabile conoscere anche i dati attesi in uscita (assumendo che il programma funzioni correttamente).

Infatti, quando eseguiamo il collaudo sui casi di prova dobbiamo confrontare i valori prodotti in output con i risultati attesi.

Possiamo usare l'opzione di redirezione dello standard output per memorizzare i dati prodotti in output dal collaudo:

   java classeCollaudo < fileDatiInput > fileDatiOutput

Per poter verificare i dati in output è quindi conveniente aver memorizzato in un file i valori attesi per ogni caso di prova. Tali valori possono essere generati:

  • manualmente
  • in modo da verificare che i dati in input e quelli in output verifichino certe proprietà (ad esempio se un array è ordinato dopo avere eseguito un algoritmo di ordinamento, oppure se moltiplicando per se stesso l'output di un metodo che calcola la radice quadrata si riottiene il valore in ingresso)
  • tramite un oracolo (un programma che assolve lo stesso scopo di quello che stiamo collaudando, magari molto più lento ma che crediamo essere più affidabile... ad esempio una versione precedente dello stesso metodo)
L'insieme delle prove da ripetere per il collaudo è detto pacchetto di prova (test suite).

È importante conservare i casi di prova e i rispettivi output:

  1. come testimonianza della validità del programma,
  2. per collaudare altre versioni del programma,
  3. per controllare se si ripresentano errori che credevamo di avere già corretto. Quest'ultimo è un fenomeno piuttosto diffuso che viene detto ciclicità (il controllo di assenza di vecchi errori viene detto collaudo regressivo).




Scatole nere e scatole bianche

Quando si collaudano le funzionalità del programma senza conoscere o considerare la sua struttura interna (es. software proprietario o troppo complesso) si parla di collaudo a scatola chiusa (black-box testing o test funzionale). Questo è il tipo di test più diffuso perché si adatta facilmente a tutti i livelli di sviluppo delle applicazioni ed è meno costoso (più facile da sviluppare) di altri tipi di test.

Per accertare il corretto funzionamento del programma su tutti i possibili valori in ingresso (positivi, limite e negativi) avremmo però bisogno di infiniti casi di prova a scatola chiusa, mentre dobbiamo accontentarci di un numero finito.

Quando si conosca la struttura del programma è invece opportuno utilizzare tecniche di collaudo trasparente (white-box testing o test strutturale), scegliendo i casi di prova in modo che ciascuna porzione del programma venga collaudata almeno una volta. Infatti, se un frammento di codice è difettoso ma nessun test lo esegue il difetto non viene rilevato. Poter offrire una migliore copertura del collaudo permette di aumentare la fiducia nella correttezza dell'intero programma.

Infine è importante focalizzare i test su difetti comunemente presenti in molti programmi, utilizzando il cosiddetto collaudo basato su difetti (fault-based testing). Ad esempio, se vengono usati array è opportuno controllare che non si verifichino tentativi di accesso a posizioni inesistenti (out-of-bounds).

Il collaudo può evidenziare solamente la presenza di errori, non la loro assenza.
                                    Edsger W. Dijkstra