Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Porfirio Tramontana » 20.Testing Black Box


Testing Black Box

Si esercita il sistema immettendo input e osservando i valori degli output.
Non conosciamo (oppure non teniamo in conto):

  • Il codice sorgente;
  • Lo stato interno dell’applicazione;
  • Il funzionamento interno dell’applicazione.

Viceversa, conosciamo e utilizziamo per la progettazione dei casi di test:

  • L’interfaccia del sistema;
  • La documentazione
    • In particolare i documenti relativi alla fase di analisi, ad esempio la descrizione degli scenari.

Criteri di copertura per il testing Black Box

Obiettivi principali del testing black box possono essere:

  • La copertura degli scenari di esecuzione definiti a margine dei casi d’uso;
  • La copertura delle funzionalità previste nella specifica dei requisiti.

Le difficoltà principali sono legate a:

  • la ricerca di casi di test che coprano i dati scenari/funzionalità;
  • la verifica dell’avvenuta copertura.

La soluzione più spesso adottata è quella di

  • Generare casi di test con un opportuno criterio;
  • Valutare l’effettiva copertura di scenari/funzionalità.

Suddivisione in Classi di equivalenza

Occorre dividere i possibili input in gruppi i cui elementi si ritiene che saranno trattati similarmente dal processo elaborativo.
Questi gruppi saranno chiamati classi di equivalenza.
Una possibile suddivisione è quella in cui classe di equivalenza rappresenta un insieme di stati validi o non validi per una condizione sulle variabili d’ingresso.

Chi esegue il testing eseguirà almeno un test per ogni classe di equivalenza.
Criterio di copertura delle classi di equivalenza.

Definizione delle classi di equivalenza

Se la condizione sulle variabili d’ingresso specifica:

  • intervallo di valori
    • una classe valida per valori interni all’intervallo, una non valida per valori inferiori al minimo, e una non valida per valori superiori al massimo;
  • valore specifico
    • una classe valida per il valore specificato, una non valida per valori inferiori, e una non valida per valori superiori;
  • elemento di un insieme discreto
    • una classe valida per ogni elemento dell’insieme, una non valida per un elemento non appartenente;
  • valore booleano
    • una classe valida per il valore TRUE, una classe non valida per il valore FALSE.

Inoltre, in ogni caso:
Una classe non valida per valore∉tipo

Selezione dei casi di test dalle classi di equivalenza

Ogni classe di equivalenza deve essere coperta da almeno un caso di test.

Un caso di test per ogni classe non valida.

Ciascun caso di test per le classi valide dovrebbe comprendere il maggior numero di classi valide ancora scoperte.

Esempio

Una condizione di validità per un input password è che la password sia una stringa alfanumerica di lunghezza compresa fra 6 e 10 caratteri.
Una classe valida CV1 è quella composta dalle stringhe di lunghezza fra 6 e 10 caratteri.

Due classi non valide sono:

  • CNV2 che include le stringhe di lunghezza <6
  • CNV3 che include le stringhe di lunghezza >10

Esercizio

Un programma C++ riceve in input una data, composta di giorno (numerico), mese (stringa che può valere gennaio … dicembre), anno (numerico, compreso tra 1900 e 2000) e restituisce il giorno della settimana corrispondente (1= lunedì, 2= martedì … 7=domenica).

Selezionare i casi di test mediante partizione in classi di equivalenza.

Applicazione da testare

Per esercizio, provare ad eseguire i test previsti sulla funzione riportata di lato.

Il codice viene riportato per comodità, ma il testing viene progettato senza analizzare il codice sorgente.


Le condizioni sull’input ‘giorno’

Condizioni d’ingresso:

  • Il giorno può essere compreso tra 1 e 31

Classi di equivalenza:

  • Valida
    • CE1 : 1 ≤ GIORNO ≤ 31
  • Non valide
    • CE2 : GIORNO < 1
    • CE3 : GIORNO > 31
    • CE4 : GIORNO non è un numero intero

Le condizioni sull’input ‘mese’

Condizioni di ingresso:
Il mese deve essere nell’insieme M=(gennaio, febbraio, marzo, aprile, maggio, giugno, luglio, agosto, settembre, ottobre, novembre, dicembre).
Classi di equivalenza
Valide
CE5: MESE ∈ M

Non valida
CE6: MESE ∉ M

Le condizioni sull’input ‘anno’

Condizioni di ingresso:
Deve essere compreso tra 1900 e 2000

Classi di equivalenza
Valida
CE7: 1900<= ANNO<=2000

Non valide
CE8: ANNO< 1900
CE9: ANNO> 2000

CE10: ANNO non è un numero intero

Scelta dei casi di test …


Dimensione della Test Suite

Ogni TC riesce a coprire almeno una classe di equivalenza non coperta da alcuno dei precedenti.

La Test Suite comprendente TC1…TC8 copre tutte le classi di equivalenza (ma non tutte le possibili combinazioni …)
Ad esempio, non viene testata la risposta del sistema ad una data di nascita come il 30 febbraio!

Per valutare la qualità della test suite bisognerebbe valutare contemporaneamente:

  • L’efficienza, in termini di casi di test che riescono a scoprire nuovi malfunzionamenti;
  • L’efficacia, in termini di malfunzionamenti trovati.

Nel nostro caso, proporre casi di test in grado di sollecitare tutte le combinazioni ammissibili degli input farebbe aumentare probabilmente l’efficacia della test suite riducendo l’efficienza.

… Scelta dei casi di test

Una Test Suite più efficiente potrebbe essere questa a lato.

Tutte le classi di equivalenza sono coperte ma …
È molto più difficile individuare gli errori.
Ad esempio in TC2 il sistema potrebbe rispondere con un’eccezione perchè il giorno é <1, senza valutare il mese e l’anno!


Problemi

A volte non é possibile determinare staticamente le classi di equivalenza. Esempio: un sistema accetta password di tipo stringa. Classi di equivalenza possono essere:

  • Classi valide:
    • CE1: PASSWORD corrispondente ad un utente che ha diritto d’accesso.
  • Classi non valide:
    • CE2: PASSWORD corrispondente ad un utente che non ha diritto d’accesso;
    • CE3: PASSWORD vuota.

Nella descrizione dei casi di test bisogna quindi tener conto di precondizioni: vedi figura.


Problemi (segue)

L’appartenenza ad una classe di equivalenza dipende quindi dallo stato dell’applicazione.

  • Non sempre è possibile determinare lo stato nè poter settare precondizioni e postcondizioni
    • In questi casi non è possibile nemmeno valutare l’efficacia del criterio, per cui l’affidabilità del test è incognita;
    • In questi casi si può solo cercare di fare quanti più test possibili, oppure ricavare i test dall’osservazione dell’utilizzo reale dell’applicazione.

Scrittura dei test di unità

Il testing a livello di unità dei comportamenti di una classe dovrebbe essere progettato ed eseguito dallo sviluppatore della classe, contemporaneamente allo sviluppo stesso della classe.
Di questa opinione sono in particolare Erich Gamma e Kent Beck, meglio conosciuti come gli autori dei Design Pattern e dell’eXtreme Programming (che verrà presentato nella lezione dedicata ai cicli di vita).

Vantaggi:
Lo sviluppatore conosce esattamente le responsabilità della classe che ha sviluppato e I risultati che da essa si attende.
Lo sviluppatore conosce esattamente come si accede alla classe, ad esempio:

  • Quali precondizioni devono essere poste prima di poter eseguire un caso di test;
  • Quali postcondizioni sui valori dello stato degli oggetti devono verificarsi.

Svantaggi:
Lo sviluppatore tende a difendere il suo lavoro … troverà meno errori di quanto possa fare un tester!

Testing Automation

Se la progettazione dei casi di test é un lavoro duro e difficile, l’esecuzione dei casi di test é un lavoro noioso e gramo!

L’automatizzazione dell’esecuzione dei casi di test porta innumerevoli vantaggi:

  • Tempo risparmiato (nell’esecuzione dei test);
  • Affidabilità dei test (non c’é rischio di errore umano nell’esecuzione dei test)
    • Si può migliorare l’efficacia a scapito dell’efficienza sfruttando il fatto che l’esecuzione dei test è poco costosa;
  • Riuso (parziale) dei test a seguito di modifiche nella classe.

Testing basato su main

Scrivere un metodo di prova (“main”) in ogni classe contenente del codice in grado di testare I suoi comportamenti.

Problemi

  • Tale codice verrà distribuito anche nel prodotto finale, appesantendolo.
  • Come strutturare i test case? Come eseguirli separatamente?

Per tutti questi motivi, cerchiamo un approccio sistematico:

  • Che separi il codice di test da quello della classe;
  • Che supporti la strutturazione dei casi di test in test suite;
  • Che fornisca un output separato dall’output dovuto all’esecuzione della classe.

Famiglia X-Unit

La soluzione alle problematiche precedenti é data dai framework della famiglia X-Unit:

  • JUnit (Java)
    • È il capostipite; fu sviluppato originariamente da Erich Gamma and Kent Beck
  • CppUnit (C++)
  • csUnit (C#)
  • NUnit (.NET framework)
  • HttpUnit (Web Application)

JUnit é un framework (in pratica consiste di un archivio .jar contenente una collezione di classi) che permette la scrittura di test in maniera ripetibile.

Componenti di un test XUnit

Una classe di test, contiene:

  • Un metodo setup() che viene eseguito prima dell’esecuzione di ogni test
    • Utile per settare precondizioni comuni a più di un caso di test;
  • Un metodo teardown() che viene eseguito dopo ogni caso di test
    • Utile per resettare le postcondizioni;
  • Un metodo per ogni caso di test.

Struttura di un metodo di test

Inizializzazione precondizioni
Limitatamente alle precondizioni tipiche del singolo caso di test, le altre potrebbero essere nel setup.

Inserimento valori di input
Tramite chiamate a metodi set oppure tramite assegnazione di valori ad attributi pubblici.

Codice di test
Esecuzione del metodo da testare con gli eventuali parametri relativi a quel caso di test.

Valutazione delle asserzioni
Controllo di espressioni booleani (asserzioni) che devono risultare vere se il test dà esito positivo, ovvero se i dati di uscita e/o le postcondizioni riscontrati sono diversi da quelli attesi.

Esempio: calcolatrice

Il metodo setUp() può essere completato, accodando tutte quelle operazioni da effettuare preliminarmente a qualsiasi test che sarà descritto in questa classe;

Il metodo tearDown() conterrà il codice relativo a tutte le operazioni comuni da effettuare dopo l’esecuzione di ogni test di questa classe (ad esempio per ripristinare lo stato della classe prima dell’esecuzione del prossimo test.


Scrittura di un caso di test

Scriviamo un metodo testSomma che rappresenti un caso di test per il metodo Somma;

Siccome il metodo appartiene ad una classe calcolatriceTest nello stesso package della classe da testare, il metodo test può istanziare oggetti della classe ed accedere ai suoi metodi.

public void testSomma() {

calcolatrice c=new calcolatrice();
int a=5,b=7;
int s=c.somma(a,b);
assertEquals("Somma non corretta!",12,s);
}

Asserzioni

Il metodo assertEquals verifica se s (valore ottenuto dall’esecuzione del metodo somma é uguale a 12 (valore atteso); in caso contrario conta questo fatto come una failure e genera il messaggio di errore indicato

assertEquals("Somma non corretta!",12,s);

Esempi di asserzione

  • static void assertEquals(boolean expected, boolean actual)
    • Asserts that two booleans are equal.
  • static void assertEquals(int expected, int actual)
    • Asserts that two ints are equal.
  • static void assertEquals(java.lang.String expected, java.lang.String actual)
    • Asserts that two Strings are equal.
  • static void assertFalse(boolean condition)
    • Asserts that a condition is false.
  • static void assertTrue(boolean condition)
    • Asserts that a condition is true.
  • static void assertNull(java.lang.Object object)
    • Asserts that an object is null.
  • static void fail()
    • Fails a test with no message.
  • static void fail(java.lang.String message)
    • Fails a test with the given message.

Test rilevanti errori …

public void testDivisione(){

calcolatrice c=new calcolatrice();
int a=15,b=2;
double s=c.divisione(a,b);
assertTrue(s==7.5);
}

public double divisione (int a, int b)
{return a/b;}

In realtà il metodo divisione restituisce la divisione intera …
Grazie a JUnit possiamo prontamente trovare il rigo con l’asserzione errata e avviare il debugging …
Quando pensiamo di aver corretto l’errore rieseguiamo il test …

Limiti di JUnit

Se una classe é in associazione/dependency con altre e JUnit rileva l’errore, esso potrebbe dipendere dall’altra classe (se non é stata testata adeguatamente) oppure dall’integrazione …
JUnit é uno strumento che é in grado di risolvere SOLO le problematiche relative al testing di unità … dà solo qualche utile indicazione rispetto al testing di integrazione!
Si é dato per scontata la correttezza del codice delle classi di test … se avessimo voluto esserne sicuri al massimo avremmo potuto fare il test di unità delle classi di test stesse (paradossale!)
Il codice delle classi di test é comunque estremamente lineare e ripetitivo: la possibilità di sbagliare é ridotta!

I materiali di supporto della lezione

Sommerville, Ingegneria del Software, 8° edizione, Capitoli 22-23-24.

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion