Sei sulla pagina 1di 11

Ingegneria del Software

Lorenzo Laneve

1 Il ciclo di vita del software


La descrizione della produzione industriale del della produzione industriale del sw, dalla sua concezione ini-
ziale, dalla sua concezione iniziale fino al suo sviluppo completo, al suo rilascio, alla sua successiva evoluzione,
al suo ritiro. Un modello di produzione consente di:

• pianificare le attività e le risorse necessarie (p.es. sapere quando servono certe parti e quante ne
servono);
• prevedere e controllare i costi del processo e la prevedere e controllare i costi del processo e la qualita
dei prodotti.

1.1 Modello Waterfall


Modello che tratta la produzione di Software come catena di montaggio. È costituito dalle seguenti fasi:

• studio di fattibilità e pianificazione: analisi costi/benefici, descrizione di possibili soluzioni con


corrispondenti stime di tempo e costi;

• analisi e specifica dei requisiti: individuazione delle caratteristiche del software e descrizione non
ambigua di esse;
• progetto: produce un ”Documento di Progetto” e definisce le componenti
– di massima (architettura): scomposizione dei moduli e delle relazioni tra essi (supporta lo
sviluppo parallelo tra essi e la suddivisione delle responsabilità);
– di dettaglio: progettazione dei singoli moduli nel dettaglio, in particolare le interfacce e le
modalità di verifica;
• implementazione e test:

– codifica: codifica dei moduli mediante opportuni linguaggi di programmazione e prime verifiche
statiche;
– test di unità: ogni modulo viene verificato e testato isolatamente;
– test di integrazione: si uniscono man mano i moduli testandone i legami, fino ad averli connessi
tutti;
– test di sistema: si convalida il sistema rispetto ai suoi requisiti funzionali;
• deployment: viene distribuito e installato il software sulle macchine dei clienti, il personale addetto
al suo utilizzo viene addestrato;
• modifica e manutenzione:

– correttiva: correzione di bug o di caratteristiche non completamente soddisfacenti i requisiti;


– adattiva: modifiche sui requisiti o sull’ambiente operativo;
– perfettiva: difetti di efficienza o di efficacia, reingegnerizzazione.

1
Le evoluzioni del software sono generalmente dovute a mutamenti nel contesto o dei requisiti, o a specifiche
errate.
Il modello a cascata presenta diversi problemi:
• è un insieme di fasi eseguite in rigida sequenza;
• la fase di manutenzione è tenuta in minore considerazione, che non viene strutturata in termini di
attività elementari;
• troppo poco pratico e realistico: non permette di parallelizzare processi che possono essere svolti
contemporaneamente (diverse parti del software);
• coinvolge poco l’utente, che potrebbe dare il suo contributo (es. trovando errori nei requisiti);

1.2 Modello Incrementale


C’è bisogno di più flessibilità e trasparenza: il processo deve poterci permettere di verificare ciò che stiamo
facendo continuamente, e correggere rotta il prima possibile. Questo si ottiene attraverso un’incrementalità:
si costruisce il software in modo incrementale in modo da poterlo verificare e far vedere all’utente man mano
che il progetto va avanti. Il rilascio incrementale si sviluppa in:

• identificazione dei subset indipendenti dell’applicazione


• sui subset si ottiene il feedback dei committenti

Durante questo processo si sviluppa anche un prototipo, che è un modello approssimato dell’applicazione.
L’incrementalità è fondamentale per una rapida uscita sul mercato e la fidelizzazione del cliente. Spesso,
i requisiti si raccolgono man mano, a ogni iterazione del modello.

2 Il linguaggio UML
Il linguaggio UML è diventato uno standard de facto per la modellizzazione di software orientati agli oggetti.

2.1 Class Diagram


Definiscono la visione statica del sistema:

• classi
• relazioni tra classi:
– associazione (uso);
– generalizzazione (ereditarietà);
– aggregazione (contenimento).

2.1.1 Classi e Interfacce


Una classe è composta da:

• nome;
• attributi (stato dell’oggetto);
• metodi (il comportamento).

Inoltre, è possibile definire la visibilità di ciascuno di essi, apponendo uno tra i simboli + (public), -
(private), # (protected), ∼ (package-local).
Il tipo degli attributi, del ritorno dei metodi, e dei loro parametri è definibile con :, mentre eventuali
componenti statiche della classe si sottolineano. Le classi possono essere raggruppate in package.

2
2.1.2 Generalizzazione
Le classi possono ereditare da altre classi (freccia continua con testa bianca) e implementare da interfacce
(freccia tratteggiata con testa bianca). UML ammette l’ereditarietà multipla.

2.1.3 Associazioni, Aggregazioni, Composizioni


Tra una o due classi ci può essere un’associazione, che associa un numero di oggetti di una classe a quelli di
un’altra. Un’associazione si rappresenta con una linea continua tra due classi, accompagnata eventualmente
da un nome.

2.2 Object Models


Gli object models descrivono il sistema in termini di classi di oggetti:

• oggetto: concetto, astrazione o entità con un significato ben preciso per l’applicazione;

• stato: condizione in cui si trova un oggetto in un determinato istante;


• comportamento: azioni e reazioni di un oggetto;
• operazione: servizio offerto da un oggetto
• classe: insieme di entità identificato da delle caratteristiche comuni;

• relazioni: tra oggetti e classi diverse.

Abbiamo quindi tre fasi:

• Object-oriented Analysis: sviluppare un object model del dominio applicativo e individuare le


responsabilità del sistema da sviluppare;
• Object-oriented Design: sviluppare un modello object-oriented del sistema (UML) che soddisfa i
requisiti;
• Object-oriented Programming: realizzare il design object-oriented codificando in un linguaggio di
programmazione orientato agli oggetti.

2.3 Sequence Diagram


2.3.1 Frame di interazione

3 Astrazioni Procedurali
• Abstraction by parameterization: generalizzare un modulo per usarlo su dati diversi (esempio con abs);
• Abstraction by specification: non importa come una funzione venga implementata, basta che si
comporta come ci si aspetta;
– località: la specifica può essere letta o scritta senza necessità di esaminare l’implementazione;
– modificabilità: reimplementazione senza intaccare il codice cliente;

Un contratto può essere soddisfatto in molti modi, ma cambiano le proprietà non funzionali (complessità).

3
3.1 JML
3.1.1 Clausole
Attensione: ricordarsi il semicolon alla fine di ogni clausola.

• requires bool : Precondizione;


• ensures bool : Postcondizione normale;
• signals (ExType) bool : Postcondizione eccezionale;
• also: Le clausole seguenti si aggiungono a quelle del metodo corrispondente nella superclasse, secondo
le regole di estensione;
• assert bool : nel codice, lancia eccezione in caso l’espressione sia falsa;
• assignable expr1, ..., exprN : expr1, ..., exprN sono modificabili dal metodo. Si può usare * come
wildcard per indici di array. Se \nothing viene usato, allora il metodo non modificherà nulla dei
parametri (default: all);
• pure: il metodo non ha side effects;
• helper : il metodo serve come algoritmo di ausilio ad altri metodi, e quindi non vengono valutati gli
invarianti della classe all’epilogo e al prologo;

• spec public T x : specifica un oggetto astratto tipico x di tipo T;


• public—protected—private invariant bool : definisce un invariante della classe. Il modificatore indica
sostanzialmente i membri della classe che possono essere usati nell’invariante;

3.1.2 Espressioni utili


• (\forall T s; filtro; condizione): ∀s ∈ T (f iltro ⇒ condizione)

• (\exists T s; filtro; condizione): ∃s ∈ T (f iltro ∧ condizione)


P
• (\sum T s; filtro; expr): s∈T ∧f iltro (expr)
Q
• (\product T s; filtro; expr): s∈T ∧f iltro (expr)
• (\max T s; filtro; expr): maxs∈T ∧f iltro (expr)

• (\min T s; filtro; expr): mins∈T ∧f iltro (expr)


P
• (\numof T s; filtro; condizione): s∈T ∧f iltro∧condizione (1)
• \old(expr): valore di expr nel pre-stato;

• \result: ritorno del metodo;


• JMLDouble.approximatelyEqualTo(x, y, ε): |x − y| < ε;

4 Astrazioni sui Dati


Le specifiche per le astrazioni procedurali non sono adatte per astrarre i dati, perché i metodi delle classi
ADT agiscono sulle variabili di stato degli oggetti.
Tra i metodi dell’interfaccia di una classe in Java, possiamo tramite JML definire puri quelli che si
limitano ad osservare l’oggetto. I tipi di metodi sono:

• creators: sostanzialmente costruttori;

4
• producers: da oggetti di quel tipo ne creano altri (esempio Poly.add(Poly) → Poly);
• mutators: modificano lo stato dell’oggetto;
• observers: si limitano a restituire informazioni sul tipo (valori di un altro tipo).

Tramite i metodi si separa l’oggetto astratto (Polinomio) dall’oggetto concreto (array di interi).
Un tipo è adeguato se fornisce operazioni sufficienti. Un buon modo di verificare l’adeguatezza è:
• se mutabile almeno creators, observers, mutators;

• se immutabile almeno creators, observers, producers;


• deve essere totalmente popolato: tramite i metodi ogni stato è raggiungibile dagli oggetti di quel tipo.
• anche se totalmente popolato, sarebbe meglio aggiungere operazioni per migliorare l’efficienta;

4.1 Oggetto astratto tipico


Il problema è che la specifica deve esprimersi esclusivamente in termini di oggetto astratto, e gli observer
della classe non sono sufficienti. (Es: Pila con top(), push(x), pop(). Non si possono scrivere specifiche
esaurienti usando solo top()).
Possiamo definire un oggetto astratto tipico, che è un oggetto fittizio che utilizziamo per ragionare sulla
specifica. Di solito si usa una List, un set o una stringa. Nell’esempio della pila possiamo usare una List
come OAT. In notazione di Hoare: {”x1, ..., xN”} push(a) {”x1, ..., xN, a”}

4.2 Proprietà astratte


Sono proprietà osservabili con gli observer, semplicemente dalla specifica dei metodi.
• Proprietà evolutive: relazione tra stato astratto osservabile e quello successivo (esempio grado di un
polinomio non cambia dopo questo metodo);
• Invarianti: proprietà degli stati osservabili (size di un insieme sempre non negativo).

Si definisce una funzione di astrazione AF : ConSt → AbsSt, che associa uno stato concreto a uno
stato astratto. Si noti che questa funzione non è necessariamente iniettiva. Esempio: un insieme di interi
non conta l’ordine, mentre l’array che lo implementa si, quindi ogni permutazione del’array è associata
tramite AF allo stesso insieme. Si può definire indirettamente AF tramite un invariante privato, definendo
la relazione tra il ritorno degli observer e i valori concreti dell’oggetto.
Definiamo un invariante di rappresentazione J : ConcSt → bool tale per cui J(a) = true ⇔ a è un
oggetto valido per la rappresentazione astratta. Infatti, AF non è totale: esistono oggetti concreti che non
hanno un corrispettivo astratto, e J è vero se e solo se AF è definito. Esempio: [1, 2, 2] come array che
rappresenta un insieme quando si è deciso di rimuovere i duplicati. L’invariante di rappresentazione si può
esprimere in JML come invariante privato.
Chiamiamo effetto collaterale benevolo un passaggio da uno stato S a uno stato T tale per cui
AF (S) = AF (T ). Servono a trasformare l’oggetto concreto, magari per ragioni di efficienza, mantenendolo
inalterato agli utilizzatori.

5
5 Design Patterns
5.1 Pattern creazionali
• Singleton: tenere privato il costruttore e dare l’accesso ad un’unica istanza della classe tramite metodo
statico (synchronized sulla creazione istanza per thread safety);
• Factory: viene definita una classe C con un metodo che crea un’oggetto di una classe T e per ogni
sottoclasse di T viene definita una sottoclasse di C che fa l’override del metodo menzionato. Al posto
di chiamare il costruttore a seconda della classe, si chiama il metodo di un oggetto generico di C;
• Abstract Factory: come Factory, ma restituisce un oggetto generico di una classe astratta, da cui
eredita una famiglia di classi da poter costruire.

5.2 Pattern strutturali


• Adapter: adattare l’interfaccia creando una classe che la implementa, riflettendo i metodi sulla classe
da adattare;
• Decorator: aggiungere funzionalità a un oggetto (component), incapsulandolo in un oggetto che
implementa una funzionalità in più (decorator). Il component è attributo del decorator, e quest’ultimo
riflette i metodi del primo (es: java.io.InputStream e FilterInputStream);
• Facade: mostrare un’interfaccia ridotta (fornita da una classe Facade) e più semplice per interagire
con una rete di classi più complessa da fuori (es: creare una classe Database che permetta al resto
dell’applicazione di interagire con il db nascondendo JDBC);
• Proxy: oggetto fittizio che permette di istanziarne un altro della stessa interfaccia con laziness,
postponendo o evitandone la creazione (Es: oggetto Foto che andrebbe scaricato);
• Flyweight: per classi di oggetti immutabili, mantenere uno e un solo oggetto per ogni classe di
equivalenza del tipo, rispetto a una prefissata relazione di equivalenza;

5.3 Pattern comportamentali


• Strategy: delegare un comportamento ad un’altra classe, lasciando l’algoritmo su un oggetto che può
essere intercambiabile (es: Character che contiene WeaponBehavior con sottoclassi che implementano
algoritmi diversi);
• Observer: oggetto che implementa un’interfaccia contenente un metodo che viene chiamato da un
subject. Quando accade un evento, il subject chiama il metodo callback su tutti gli observer che
l’hanno richiesto;
• State: al posto di usare un enum e uno switch, si usa una classe astratta stato con sottoclassi che
implementano uno stesso metodo. Inoltre, si pensi che si può sfruttare il pattern State per avere
implementazioni di una classe dinamicamente intercambiabili;
• Iterator: definisce un’interfaccia standard per scorrere le collezioni;

5.4 Pattern architetturali


• Client-Server: abbiamo un modulo che fa da server a tanti client utilizzando invocazione di metodi;
• Client-Server a tre livelli: si divide il software in tre layer: presentazione (UI), logica applicativa,
e storage;
• Model-View-Controller: si divide la GUI in tre ruoli principali:
– Model: fornisce i metodi per accedere ai dati utili all’applicazione;
– View: si occupa di visualizzare i dati contenuti nel model e di interagire con utenti e agenti;
– Controller: riceve i comandi dell’utente e modifica di conseguenza lo stato di model e view.

6
6 Design Principles
• Principio Open-Closed: ogni classe deve essere aperta alle estensioni ma chiusa alle modifiche (se
non modifico la classe non rischio di fare errori);
• Principio di Sostituzione di Liskov: gli oggetti della sottoclasse devono rispettare il contratto della
superclasse, in modo che qualsiasi codice che utilizza un oggetto della superclasse possa utilizzare un
oggetto della sottoclasse senza differenze;

• Principio di Inversione di Dipendenza: le classi dovrebbero dipendere da classi astratte o inter-


facce e non il contrario; Tutte le volte che bisogna usare un riferimento a una classe, se essa è concreta,
conviene porre tra questa e la sua superclasse una classe astratta A e usare un riferimento ad essa nel
codice;
• Principio di Segregazione di Interfaccia: meglio avere più interfacce specifiche per i client che
una sola general purpose interface;
• Principio di Dipendenza Aciclica: il grafo delle dipendenze tra classi deve essere un DAG, niente
cicli;
• Principio di Dipendenza Stabile: se si hanno due classi A e B tali che A dipende da B, allora B
non deve essere meno stabile di A;

7 Qualità del Software


Oltre alla correttezza funzionale, che è necessaria, possiamo valutare il software anche in termini di requisiti
non funzionali, come l’efficienza, o di requisiti strutturali, come modificabilità e stabilità.

7.1 Indicatori strutturali


• Accoppiamento: due classi sono altamente accoppiate se sono fortemente dipendenti l’una dall’altra.
Un alto accoppiamento è male poiché implica difficoltà di modificare e comprendere le classi coinvolte.
serve basso;
– Accoppiamento di Interazione: i metodi di una classe chiamano i metodi di altre classi. Per
ridurre l’accoppiamento, passare solo l’informazione necessaria con il minimo numero di parametri;
– Accoppiamento di Componente: una classe ha attributi, parametri, o variabili locali di altri
classi;
– Accoppiamento di Ereditarietà: una classe eredita da un’altra. Andrebbero evitati override
di metodi già implementati, o modifiche alla signature di metodi della superclasse;
• Coesione: un modulo ha una forte coesione se contiene solo classi che sono altamente correlate. Questo
permette di capire al volo l’astrazione a cui il modulo si riferisce. Solitamente, un’alta coesione implica
basso accoppiamento tra i moduli;

– Coesione di Metodo: la procedura implementa una singola e ben definita funzione, descrivibile
con una sola e semplice frase;
– Coesione di Classe: la classe rappresenta un solo concetto e tutti gli attributi contribuiscono
alla sua rappresentazione. Diversi concetti rappresentati dalla classe o gruppi di metodi diversi
che lavorano con insiemi di attributi disgiunti sono sintomi di bassa coesione;
– Coesione di Ereditarietà: in generale, ereditarietà introdotte per generalizzazione/specializza-
zione implicano coesione maggiore di quelle introdotte per riuso del codice;

7
7.2 Metriche del Software
Definizione con caratteristiche comuni nei software professionali:
• Weighted Methods per Class (WMC): numero dei metodi pesati per la loro complessità;
– la maggior parte delle classi ha pochi metodi;
– più WMC è alto, più il software è propenso ad avere errori.
• Depth of Inheritance Tree (DIT)
– massimo 10;
– la maggior parte delle classi non hanno sottoclassi e sono dirette alla radice;
• Number of Children (NOC)
– spesso numero limitato di sottoclassi, in molti casi 0 (ereditarietà non sfruttata a pieno);
• Coupling Between Classes (CBC)
– spesso le classi sono auto-contenute (CBC = 0);
– gli oggetti di interfaccia tendono ad avere un CBC più grande.
• Response For a Class (RFC): somma di tutti i metodi invocabili dagli oggetti della classe (metodi
chiamati nel codice + metodi della classe stessa);
– la maggior parte delle classi invoca pochi metodi esterni;
– le classi di oggetti di interfaccia hanno RFC maggiore;
• Lack of Cohesion in Methods (LCOM): quanto i metodi di una classe accedono ad attributi
comuni.
– non molto correlato alla error-proneness;
Se un software è progettato male, esso tende a presentare sintomi come:
• Rigidità: difficoltà a cambiarlo, provoca cambiamenti a cascata;
• Fragilità: anche per un minuscolo cambiamento, tende a rompersi in molti punti;
• Immobilità: non si riesce a riusare software da altri progetti o moduli;
• Viscosità: quando è più facile usare scorciatoie che i metodi che rispettano la struttura del progetto.
Per ovviare a questi problemi, bisogna migliorare le dipendenze tra classi:
• minimizzare l’interfaccia delle classi;
• pochi parametri per i metodi;

7.3 Anti-Patterns
• classe Blob che contiene la maggior parte della logica applicativa;
• codice duplicato;
• metodi lunghi;
• mix di livelli di astrazione all’interno dello stesso metodo;
• far fare a un metodo più cose (ogni cosa un metodo);
• metodo che usa principalmente i dati di un’altra classe (fare un metodo in quella classe);
• uso dello switch per discriminare i tipi (sfruttare il polimorfismo);
• blocchi di dati (dati correlati dovrebbero stare in una classe a parte);
• commenti (racchiudere parti di codice in metodi con nome significativo);

8
8 Estensione vs Astrazione
Se vogliamo estendere una classe, dobbiamo stare attenti a non violare il principio di sostitusione di Liskov.
Questo implica che tutti i contratti della superclasse devono essere compatibili con quelli delle sottoclassi:
• Regola della segnatura: il metodo delle classi ereditate non cambia la segnatura;
– è possibile restringere il tipo di ritorno a un sottotipo (covarianza del risultato);
– è possibile allargare il tipo dei parametri a un supertipo (controvarianza dei parametri, NON
consentito in Java);
• Regola dei metodi: il contratto dei singoli metodi ereditati è compatibile con il contratto dei metodi
originali;
– i metodi ereditati devono rispettare la specifica degli originali, eventualmente indebolendo la
precondizione o rafforzando la postcondizione (estensioni impure);
– P REsuper ⇒ P REsub ;
– P OSTsub ⇒ P OSTsuper ;
– attenzione alle eventuali signals della superclasse.

• Regola delle proprietà: verifica che la specifica nel complesso sia compatibile con quella della
superclasse.
– tutti i metodi ereditati, nuovi o ridefiniti, inclusi i costruttori, devono conservare le proprietà
invarianti ed evolutive osservabili dagli observer della superclasse.

8.0.1 Esempio con also

// s u p e r c l a s s e
@ r e q u i r e s A;
@ ensures B;

// s o t t o c l a s s e
@also
@requires C;
@ e n s u r e s D;

// r i s u l t a t o
@requires A | | C;
@ e n s u r e s ( \ o l d (A) ==> B) && ( \ o l d (C) ==> D) ;

9
9 Programmazione Concorrente in Java
Ricordare sempre di porre wait e notify sotto synchronized, e di catturare/rilanciare InterruptedException

• Lock.tryLock(), Lock.lockInterruptibly()

• Executor.execute(Runnable)
• Remote Method Invocation: stub, skeleton, proxy.

10 Programmazione Funzionale in Java


La programmazione funzionale è utile perché riduce il gap tra specifica e implementazione, nasconde aspetti
di basso livello, facilita test e manutenzione, sfrutta al meglio il parallelismo, e traduce facilmente i design
pattern in implementazioni.

10.1 Metodi utili


Metodi di Stream:
void Stream<T>.forEach((T) -> void);
Stream<T> List<T>.stream();
Stream<T> List<T>.parallelStream();
Stream<T> Stream<T>.filter((T) -> boolean);
Stream<R> Stream<T>.map((T) -> R);
Optional<T> Stream<T>.reduce(T, (T, T) -> T);
Stream<T> Stream<T>.sorted();
Stream<T> Stream<T>.sorted((T, T) -> int); (se l’intero è negativo, LHS viene prima di RHS)
Stream<T> Stream<T>.findFirst();
Stream<T> Stream<T>.findAny();
long Stream<T>.count();
Stream<T> Stream<T>.distinct(); (rispetto a T.equals(T))
Stream<T> Stream<T>.concat(Stream<? extends T>, Stream<? extends T>);
boolean Stream<T>.allMatch((T) -> boolean);
boolean Stream<T>.anyMatch((T) -> boolean);
Altri metodi:
boolean Optional<T>.isPresent();
T Optional<T>.get();

11 Testing
• Verifica: il programma soddisfa le specifiche?
• Testing: verifica sperimentale facendo girare il software con degli input selezionati
• Prova di correttezza: argomentare in modo formale o informale che il programma è corretto

• Convalida: assicurarsi che le specifiche o il sistema completo rispondono alla richiesta del committente
• Debugging: localizzare errori o difetti nel codice (il testing ne rivela la presenza ma non li localizza)
• Programmazione difensiva: insieme di tecniche che cercano di evitare il più possibile gli errori
durante la scrittura del codice

10
Il testing si dice esaustivo se comprende tutti i possibili input del programma (intrattabile in generale).
Un test può essere usato come controprova di correttezza, ma mai come prova. Dobbiamo trovare i casi di
test che massimizzino la probabilità di errore.
Dobbiamo scegliere opportuni casi di test, che siano sufficienti a ”convincerci” che il programma è corretto.
Si possono generare casi di test:
• In maniera casuale: evita le polarizzazioni del progettista, ma non esplora valori particolarmente
significativi;
• In maniera sistematica: partizioniamo lo spazio di input per ottenere casi di test con un particolare
obiettivo
– funzionale: in base a ciò che il componente deve fare (black-box);
– strutturale: in base alla struttura dell’implementazione (white-box).

11.1 Black-box Testing


Si utilizzano le specifiche per partizionare lo spazio degli input. Questo significa che il Black-box Testing è
implementation-independent.
• Scomposizione della Specifica
• Identificazione dei Valori
• Introduzione dei Vincoli

11.2 White-box Testing


Si cerca di partizionare i casi di test in base al cammino che fanno percorrere al programma nella implemen-
tazione. Quindi ci poniamo il problema di trovare una copertura dei cammini, ovvero, per un programma
P(x) trovare un insieme S (preferibilmente a minima cardinalità) tale per cui per ogni cammino possibile
del codice esiste z in S che induce il programma a percorrere quel percorso specifico. Nella pratica, questo
problema è impossibile da risolvere.
• per un ciclo dovremmo generare ∼ nm casi di test, dove n è il numero di cammini del corpo dell’itera-
zione, e m è il numero massimo di passi che il ciclo può compiere.
Diciamo che un insieme di test case è inadeguato se parti significative della struttura del programma non
sono coperte dai casi di test.
• Copertura delle istruzioni: selezionare un insieme T di test case per un programma P tali per cui,
per ogni istruzione S in P esiste un test case in T che induce P ad eseguire S. Il seguente indice dice
quanta copertura delle istruzioni abbiamo (NOTA: può esistere codice morto che impedisce di avere
100%):
Ncoperte
Iistr = Ntotali

• Copertura delle diramazioni: selezionare un insieme T di test case per un programma P tali per
cui, per ogni salto B in P esiste un test case in T che induce P ad eseguire B e uno che induce P a non
eseguirlo. Il seguente indice dice quanta copertura dei branch abbiamo:
Ncoperte
Ibr = Ntotali

• Copertura delle condizioni: circa come la copertura delle istruzioni. Bisogna fare in modo che i
test case raggiungano tutte le possibili condizioni dei costituenti della condizione, in modo da evitare
che alcune parti della condizione non vengano mai eseguite per colpa della short-circuit evaluation.
• Copertura delle cammini
In conclusione, il Black-box Testing è più semplice ed intuitivo, ma il white-box Testing permette di
complementarne i risultati ed avere maggiore confidenza sulla correttezza.

11

Potrebbero piacerti anche