Swappa : Uni / Ingegneria del Software - Appunti del 5 Maggio 2009
Creative Commons License

Torna alla pagina di Ingegneria del Software

 :: Ingegneria del Software - Appunti del 5 Maggio 2009 ::

La lezione di oggi è stata un po' caotica e poco organizzata. Il suo scopo è stato quello di presentare una serie di concetti introduttori rispetto alle pratiche di testing. Per questo motivo ci sono tanti argomenti che continuano a riprendersi l'un l'altro.

Concetti iniziali

Requisiti

I requisiti sono di due tipi: funzionali e non funzionali. Quindi, dovremo avere intuitivamente due tipi di test: dei test funzionali e dei test non funzionali.

Questi due tipi di test si differenziano subito, perché:

Granularità

Un altro concetto importante è la granularità del testing. Riprendiamo un attimo i nostri bei diagrammi UML: ho le classi, i componenti, i sottosistemi, il sistema, l'utente. Insomma, ho una scala gerarchica che parte dal singolo metodo di una classe ed arriva ad un sistema installato su di una rete.

Il processo di test in generale consiste in:

  1. preparare un caso di test
  2. somministrarlo in qualche modo al software
  3. valutarne i risultati

Tutti questi passaggi vanno ripetuti a tutti i livelli di granularità, e ovviamente ci saranno differenze sul come preparare e somministrare i casi di test. Un'abilità tipica del software engineer è quella di saper preparare piani di test ad ogni livello di granularità.

Copertura

La copertura può essere vista come un criterio per valutare un test.

Un test controllerà un certo numero di requisiti: quanti ne controlla è appunto la copertura del test rispetto ai requisiti.

Un test controlla un certo numero di valori di input di un metodo: quanti ne controlla è la copertura del test rispetto ai valori di input.

Vediamo quindi che il concetto di copertura dipende dal livello di granularità a cui stiamo operando: se parlo di classi, allora coprirò gli input, se parlo di sistema allora coprirò i requisiti funzionali e così via.

Regressione

Esistono anche i test di regressione. Nei processi di sviluppo iterativi, ad ogni giro si riprende in mano il tutto e lo si modifica. Ma ogni volta che si modifica del codice, potrebbe essere possibile alterare il comportamento del software, in modo tale che un test che prima mi diceva che tutto andava bene, ora mi dirà il contrario. Posso quindi alterare la capacità del mio software di soddisfare i requisiti.

Teoricamente dovrei ripetere tutti i test ad ogni giro, ma questo oltre che dispendioso è inutile: se so dove sono state eseguite le modifiche, allora posso andare a ripetere solamente quei test che interessano il codice modificato.

Modelli

Come dicevamo la lezione scorsa, non posso pretendere che il rispetto di un modello garantisca l'assenza di errori nel mio software.

C'è gente che ha provato a scrivere modelli in un linguaggio matematico, il quale, tradotto poi in codice sorgente, permetteva di dimostrare a priori sul modello la funzionalità e la correttezza del codice.

Purtroppo questa via, oltre ad essere ancora oggetto di ricerca, è difficilmente praticabile, perché è oggettivamente difficile poter modellare tutti i requisiti in un diagramma, e soprattutto così facendo il modello assumerebbe delle proporzioni leviataniche.

Paradosso del testing

Qualsiasi metodo utilizzato per individuare certi fault lascerà un residuo di fault per i quali il mio metodo non è efficace.

È paradossale, ma è logico: dal momento che faccio un test per cercare di individuare un certo tipo di errori, ce ne saranno degli altri che per forza di cose dovrò lasciare fuori, dal momento che di test esaustivi non posso farne.

Inoltre, potrebbe essere considerata un paradosso anche la definizione di test, secondo la quale un test ha successo se evidenzia una failure. Lo scopo è dimostrare di aver sbagliato, e se lo facciamo siamo contenti.

In effetti, il punto di vista umano è diametralmente opposto. Il capo direbbe: "Se siete programmatori così bravi come dite di essere, non fareste tutti questi errori!". E il programmatore direbbe: "Fare test su del codice così semplice è fuori discussione, lo considero un'offesa personale!". Date queste premesse socio-psicologiche, ci rendiamo conto che il testing è indispensabile.

Vocabolarietto

Il testing ha un uso di parole tutte sue. Dovremo vederne un bel po', man mano che si va avanti.

La catena logica degli eventi è quindi Fault => Errore => Failure. Tuttavia, il test procede all'incontrario: fa di tutto per provocare una Failure; fatto questo, spetta a noi identificare il codice di errore che provoca il comportamento scorretto, e poi scovare il punto nel codice in cui tutto ciò è stato originato.

E teniamo anche a mente che non è per niente detto che un fault si traduca automaticamente in una failure. Se per esempio il mio codice presenta dei difetti solo con certi valori di input, e quei valori di input non vengono mai immessi, allora non mi accorgerò mai di questo difetto.

Rifacendoci alla simpatica legge di Pareto, possiamo anche inferire che i primi fault è facile scovarli: saltano fuori all'inizio e quindi il tempo ed il denaro spesi per trovarli sono pochi. Ma il brutto nasce quando sono rimasti pochi fault: ci metterò una quantità di tempo più che lineare per trovarli, perché sarà difficile far sì che il programma generi una failure!

La reliability è invece l'inverso della probabilità di avere una failure.

Tipi di testing

I tipi di testing possono essere divisi in due categorie, a seconda di come li si effettuano.

Execution-based testing = sono tutti quei test che vengono effettuati, a qualsiasi livello di granularità, eseguendo effettivamente il codice con certi input. Ci sono tutta una serie di tecniche e di tools che permettono di testare singole classi, componenti etc.

Non-execution-based testing = sono quei test che si verificano senza eseguire il codice. È possibile effettuarli, anche se il limite ovvio è che il codice non viene effettivamente eseguito. Ecco i tre tipi principali:

Possiamo anche guardare a questa distinzione tra execution-based e non-execution-based testing tramite i concetti di blackbox e whitebox.

Il perché dei nomi blackbox e whitebox si spiega facilmente: se una cosa è black, non posso guardarci dentro. Il contrario di black è white, e quindi posso guardarci dentro. Non che il bianco sia trasparente, ma tra whitebox e transparentbox sicuramente whitebox suona meglio.

I metodi blackbox sono più fattibili di quelli whitebox. Analizzare del codice è un'operazione costosa e richiedente molto tempo. Inventare casi di test, cioè valori di input, secondo certi criteri è invece più ragionevole. Vedremo più in là quali siano questi criteri.

Altre distinzioni

C'è un altro tipo di distinzione delle tipologie di testing, in base a quale parte del software si sta guardando:

Categorie (divisione per granularità)

Nelle grandi aziene in generale i vari test sono divisi in categorie che derivano dal livello gerarchico che si va ad analizzare. Ogni categoria userà un tipo diverso di testing.

In generale lo fa un tester (anche se spesso, come dicevamo, non esiste una figura separata), perché occorre una visione più globale del tutto.

Ancora vocabolario

A questo punto, possiamo ampliare il nostro vocabolario con altri termini del mondo del testing.

Stub e Driver sono due aspetti della stessa medaglia: l'obiettivo è provare qualcosa che dovrebbe vivere in un contesto, ma al momento il contesto è incompleto. Lo stub è un completamento provvisorio verso il basso, cioè verso la granularità più fine. Il driver è un completamento provvisorio verso l'alto, cioè verso granularità più grossa.

Nel caso della programmazione ad oggetti, si usano i mock object, che sono l'equivalente dei driver e degli stub ma orientati agli oggetti. In teoria il sito www.mockobject.com dovrebbe contenere una spiegazione della differenza tra i mock object e i driver e gli stub, ma il dominio è attualmente in vendita.

Categorie di fault

Ci sono diversi tipi di fault, ovvero diversi tipi di errori che possono essere commessi nello scrivere il codcie. Poter catalogare i fault è utile, perché sapendo dove il programmatore in genere sbaglia, so anche che tipo di test fare per far saltar fuori la magagna.

E quando finiscono i test?

Mai!

Dal momento che dovrei testare tutti gli input, e non posso farlo, allora in linea di principio non posso mai terminare i test.

Ci sono anche tecniche che permettono stocasticamente di predire il rischio di lasciare fault in un software, dato un programma e un caso di test non esaustivo. Ma è ancora area di ricerca, e a queste cose noi pragmatici unicremaschi preferiamo i criteri di copertura, che vedremo nelle prossime lezioni o, per chi è curioso, qui.

Valori critici e valori tipici

Cerchiamo di immaginare spazialmente lo spazio dei valori dei tipi che il mio codice accetta in input. All'interno di questo spazio, immaginiamo di definire un'area finita che rappresenta il range di valori che il mio codice si aspetta.

L'esperienza insegna che i valori critici sono i valori che stanno ai limiti di quest'area. Se il mio programma calcola fatture, che vanno da 0 a 1 milione di euro, è più probabile che ci saranno errori nel caso di 0 € e nel caso di 1.000.000 €. Anglofonicamente parlando, i valori critici sono quelli che stanno sul boundary.

Tuttavia, non possiamo accontentarci di considerare solo i valori critici. Dobbiamo sicuramente dare importanza ad essi, ma non dobbiamo affatto dimenticarci dei valori tipici, ovvero quei valori sui quali tipicamente il mio software lavorerà.

Pertanto, un qualsiasi metodo per generare casi di test dovrà generarli in modo che la maggior parte della copertura riguardi i casi critici, ma non dovrà nemmeno trascurare i casi tipici.


Torna alla pagina di Ingegneria del Software

(Printable View of http://www.swappa.it/wiki/Uni/IDS-5Maggio)