Test e validazione 1 La fase di test Il test è il processo di verifica di un programma con lo scopo di individuare errori prima della consegna all’utente finale. Tipicamente, correggere i difetti sul software evidenziati dopo il rilascio è molto svantaggioso in termini d’immagine e soprattutto economici Il software deve quindi essere testato e collaudato per la ricerca di errori commessi durante la fase di progetto e di realizzazione. Normalmente, secondo Glenford J. Myers, un programmatore più o meno esperto individua solo il 50% delle categorie di test necessarie. Questo dimostra che il test è tutt’altro che banale e va affrontato in maniera sistematica, possibilmente automatizzato e soprattutto documentato. Lo scopo di questa fase, a cui si dedica a seconda delle risorse tra il 30% e il 40% della durata dell’intero progetto (quasi la metà), è proprio quello di ridurre il numero di errore nel software a un livello accettabile; in questo aiuta l’idea che un test fallisce quando non trova errori e che un programmatore non debba testare i propri moduli. L’obiettivo di questo documento è quello di descrivere il piano di test del software adottato per il collaudo del sistema “Monopoli” definendo quali sono stati i moduli testati, le tecniche utilizzate e l’analisi dei risultati ottenuti. 1.1 Tecniche Esistono varie tecniche per test del software, di queste alcune fanno uso di un calcolatore, altre tengono conto semplicemente di situazioni comuni che tendono a verificarsi di cui è utile tenerne conto per avere una prima scrematura dei difetti più evidenti. Queste sono le principali tecniche utilizzate in questo progetto: • Ispezione: Questa tecnica viene tipicamente impiegata subito dopo la fine della codifica e va considerata preliminare al test automatizzato. Il programmatore, aiutandosi con una lista di errori ricorrenti (checklist), ispeziona il codice da testare alla ricerca soprattutto di: errori di calcolo, errori di confronto, errori di interfaccia...; per questo motivo si parla di “test umano”. • Test white box: Il test white box (scatola bianca) è un test strutturale che si esegue guardando dentro il codice da testare, per questo può essere progettato soltanto quando il codice è disponibile. Un approccio topologico per il progetto di test a scatola bianca si basa sul grafo di controllo del programma composto da un nodo per ogni istruzione atomica ed un arco per ogni transizione. I casi di test vengono definiti da determinati cammini sul grafo seguendo il criterio di copertura di condizioni e decisioni che permette di ricavare il maggior numero di errori possibile. Il numero di cammini da cercare deve essere pari alla complessità ciclomatica, un valore ottenuto da: CC = Archi – Nodi + 2 Una volta ricavati i cammini, è utile sapere se questi formano una base ovvero sono tra loro linearmente indipendenti. Questo si ottiene calcolando il rango della matrice di incidenza, in cui sono riportati, per ogni cammino, tutti gli archi ricoperti, e controllando che sia massimo. • Test black box: Il test black box (scatola nera) è un test funzionale che testa la logica del programma, complementare al test white-box e assolutamente non alternativo. Il dominio di input e di output della funzione viene partizionato in classi di equivalenza: una serie di insiemi di dati che auspicabilmente provocano lo stesso comportamento, e possono essere valide o non valide. Le intersezioni tra le classi possono essere sfruttate tra classi valide e testarle con un solo input, mentre vanno evitate, ammesso che sia possibile (inclusione) per le classi non valide perché è importante sapere dove ricade l’errore. I criteri da seguire per individuare le classi di equivalenza sono: intervalli di valori, numero di valori, insiemi di valori, condizioni vincolanti. Per la sua natura, questo tipo di test può e va progettato prima della codifica, appena i requisiti sono stabili, in modo da programmare sapendo quali difetti ci si aspetta che vengano trovati. Abbiamo deciso di non fare il test walkthrow onestamente per mancanza di tempo ma anche e soprattutto perché ci siamo accorti di avere affrontato (forse sbagliando) lo sviluppo dell’applicazione in maniera abbastanza corale coinvolgendo tutti i membri in praticamente tutti i moduli del programma. Per questo sarebbe un po’ venuto meno lo scopo di questo lavoro basato proprio sul fatto che vada ispezionato codice altrui senza conoscerne la struttura e il contenuto. 1.2 Strategie Il principio di una buona strategia per il collaudo deve avere prove a basso livello per verificare che un piccolo segmento di codice sorgente sia stato realizzato in maniera corretta e prove ad alto livello capaci di assicurare che i requisiti utente siano stati soddisfatti a pieno. Le fasi seguite sono state rispettivamente: • Revisione: La fase propedeutica al test intensivo è quella di revisionare il codice attraverso ispezione e test umano. La checklist che abbiamo utilizzato è la seguente: 1. 2. 3. 4. 5. Correttezza dei commenti Errori di interfaccia tra pagine web e servlet, tra servlet e JSP… Errori di cast Errori di interfaccia tra i moduli Ridurre la ridondanza Questo tipo di lavoro è stato svolto da ognuno di noi durante l’implementazione su proprie porzioni di codice stabile. • Test di unità: Il test di unità verifica ciascun modulo separatamente, assicurandone il corretto funzionamento come unità distinte. Si adatta bene ad un assemblaggio incrementale e fa largo uso delle tecniche white box e black box. • Test di integrazione: Il test di integrazione si specializza sull’interazione tra i moduli proprio perché molti errori si nascondono nel passaggio scorretto di parametri e nella errata interpretazione dei valori di ritorno. Per questo tipo di test è utile avere a disposizione l’albero di chiamate delle funzioni e richiede che vengano progettati dei moduli aggiuntivi che simulano il calcolo sui valori di ritorno (stub) e il passaggio corretto di parametri (driver). • Test di accettazione: Eseguito a valle del test di sistema (che nel nostro caso non è previsto), il test di accettazione verifica l’effettiva funzionalità del prodotto simulando un tipico scenario di utilizzo. • Test di regressione: È la fase che tipicamente troviamo alla fine di ogni step di test e controlla che tutte le correzioni apportate al codice non abbiano prodotto altri problemi. Questo si ottiene rieseguendo tutti o parte dei test definiti nelle fasi precedenti. Al termine della fase di test il software e pronto per la consegna ed entra a in manutenzione. 1.3 Tool I tool di cui ci siamo serviti per implementare i casi di test pianificati sono: JUnit : JUnit (www.junit.org) è un framework java da utilizzare per il test di unità. Mette a disposizioni funzioni per inizializzare le strutture dati secondo i modi previsti e per testare condizioni, valori di ritorno ed eccezioni. Jakarta Cactus: Cactus (jakarta.apache.org/cactus) è un’estensione di JUnit per testare oggetti server-side (servlet e JSP). Prevede infatti la possibilità di definire le strutture dati utilizzate da questi oggetti (es: session, request, response...) e di verificare la pagina di risposta. 2 Pianificazione ed esecuzione dei test Si passa ora alla trattazione degli aspetti legati alla pianificazione. La pianificazione dei test è un concetto che va oltre la definizione di cosa testare e come testarlo, ovvero di quelle attività che tipicamente vanno sotto il nome di “piano dei test”; ma si occupa di tutti gli aspetti coinvolti nel progetto dei test, nella loro esecuzione e nella correzione. Per raggiungere un buon livello di qualità e di descrizione, è importante stabilire dettagliatamente la strategia d’azione di come definire e implementare un caso di test, come comportarsi nell’eseguirlo, come affrontare le correzioni di eventuali errori e cosa fare a seguito di queste. Tutto ciò deve essere poi supportato da un’equa suddivisione del lavoro definendo responsabilità e referenti di ogni attività. 2.1 Piano dei test Il piano dei test ha preso in considerazione soltanto gli scenari e le funzioni più importanti dell’applicazione, le funzioni sono state scelte però in modo tale da sfruttare il più possibile analogie strutturali e funzionali in modo da propagare, entro certi limiti, il test a più componenti. Questo significa però stare più attenti alle correzioni apportate, lavorando bene con il test di regressione per non rischiare di allargare possibili malfunzionamenti ad altre unità. Da notare che tutte queste attività tralasciano il test del codice HTML. Questo produce problemi soprattutto nell’annidamento di tag e sugli script di controllo. Gli errori più evidenti sono stati comunque rivisti con il test di accettazione. 2.2 Test black box Test del metodo login di ControllerUtente 1. Tecnica del test: black box 2. Descrizione 3. Segnatura: boolean login(Utente u) 4. Obiettivo: il metodo controlla se per lo username in questione la password corrisponde a quella inserita in fase di registrazione 5. Parametri 6. Utente u 7. Valore restituito: booleano (true se la password è corretta) 8. Definizione delle classi d'equivalenza ID Classe Valida/Non Valida C1 Utente presente sul DB Valida C2 Utente = null Non Valida C3 Utente non presente sul DB Valida 1. Definizione dei casi di test 1. Test Case TL1 1. Copre la classe C1 2. Input: utente1 3. Output atteso: true 2. Test Case TL2 1. Copre la classe C2 2. Input: null 3. Output atteso: java.lang.NullPointerException.class ■ N.B. Il caso di un utente = null viene testato ma non si può comunque presentare poiché i controlli vengono preliminarmente effettuati dal codice javascript della pagina di registrazione 3. Test Case TL3 1. Copre la classe C3 2. Input: utente2 3. Output atteso: false Test del metodo insert di DaoUtente 1. Tecnica del test: black box 2. Descrizione 1. Segnatura: void insert (Utente u) 2. Obiettivo: inserire un nuovo utente nella tabella Utente 3. Parametri 1. Utente u 4. Valore restituito: nessun valore 3. Definizione delle classi d'equivalenza ID Classe Valida/Non Valida z C1 Utente valido Valida C2 Utente = null Non Valida Definizione dei casi di test c Test Case TI1 ■ Copre la classe C1 ■ Input: utente ■ Output atteso: nessun output c Test Case TI2 ■ Copre la classe C2 ■ Input: null ■ Output atteso: RuntimeException.class ■ N.B. Il caso di un utente = null viene testato ma non si può comunque presentare poiché i controlli vengono preliminarmente effettuati dal codice javascript della pagina di registrazione Test del metodo nickPresente di DcsUtente z Tecnica del test: black box z Descrizione c Segnatura: boolean nickPresente (Utente u) c Obiettivo: il metodo controlla se per lo username in questione l'utente corrispondente è presente nel DB c Parametri: ■ Utente u c Valore restituito: booleano (true se l'utente è presente nel DB) z Definizione delle classi d'equivalenza ID Classi Valida/Non Valida z C1 Utente presente sul DB Valida C2 Utente = null Non Valida C3 Utente non presente sul DB Valida Definizione dei casi di test c Test Case TN1 ■ Copre la classe C1 ■ Input: utente1 ■ Output atteso: true c Test Case TN2 ■ Copre la classe C2 ■ Input: null ■ Output atteso: java.lang.NullPointerException.class ■ N.B. Il caso di un utente = null viene testato ma non si può comunque presentare poiché i controlli vengono preliminarmente effettuati dal codice javascript della pagina di registrazione c Test Case TN3 ■ Copre la classe C3 ■ Input: utente2 ■ Output atteso: false 2.3 Test white box Servlet ServHome /*inizio*/ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /*1*/ HttpSession sessione=request.getSession(true); ServletContext sc=getServletContext(); String user=request.getParameter("T4"); String pwd=request.getParameter("T5"); String address = null; Utente u=ControllerUtente.getUtente(user, pwd); /*2*/ if(ControllerUtente.login(u)){ /*3*/ if(!ServletUtil.login(u,sc) ){ /*4*/ sc.setAttribute(u.getnick(),u); sessione.setAttribute("utente",u); address="Redirect6.htm"; } else{ /*5*/ sessione.setAttribute("err","Utente Loggato"); address="ERRORIACCESSO.jsp"; } } else{ /*6*/ sessione.setAttribute("err"," Nick o Password errati\\no registrazione non effettuata"); address="ERRORIACCESSO.jsp"; } /*7*/ /*fine*/ Grafo: RequestDispatcher dispatcher=request.getRequestDispatcher(address); dispatcher.forward(request, response); } Complessità Ciclomatica: V(G)=3 Cammini Ricoprenti: 9. C1: a-b-c-e 10. C2: a-b-d-f 11. C3: a-g-h Matrice d'incidenza: a b c C1 X X X C2 X X C3 X coperto X X d e f g h X X X X X X X X X X X Casi Testati: ID Input Output Esito TSHC1 Utente u є DB, u.getpass() = DcsUtente.getPassword(u), u.getnick() !є contesto Redirect6.htm OK TSHC2 Utente u є DB, u.getpass() = DcsUtente.getPassword(u), u.getnick() є contesto ERRORIACCESSO.jsp OK TSHC3 Utente u є DB, u.getpass() != DcsUtente.getPassword(u) ERRORIACCESSO.jsp OK Servlet ServReg /*inizio*/ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /*1*/ HttpSession sessione=request.getSession(true); String nome=request.getParameter("T2"); String cognome=request.getParameter("T3"); String email=request.getParameter("T4"); String nato=(request.getParameter("D2")+"/"+request.getParameter("D3")+"/"+request.getP arameter("D4")); String nick=request.getParameter("T5"); String pwd=request.getParameter("T7"); Utente utente= new Utente(nome,cognome,nato,email,nick,pwd); String address=null; /*2*/ if(!ControllerUtente.nickPresente(utente)){ /*3*/ if(ControllerUtente.registrazione(utente)){ /*4*/ ControllerUtente.insert(utente); address="Redirect2.htm"; } else{ /*5*/ sessione.setAttribute("err","Utente già registrato"); address="ERRORIACCESSO.jsp"; } } else{ /*6*/ /*7*/ /*fine*/ sessione.setAttribute("err","Nick già in uso"); address="ERRORIACCESSO.jsp"; } RequestDispatcher dispatcher=request.getRequestDispatcher(address); dispatcher.forward(request, response); } Grafo: Complessità Ciclomatica: V(G)=3 Cammini Ricoprenti: z C1: a-b-c-e z C2: a-b-d-f z C3: a-g-h Matrice d'incidenza: a b c C1 X X X C2 X X C3 X coperto X X d e f g h X X X X X X X X X X X Casi Testati: ID Input Output Esito TSRC1 u.nick !є DB && tupla <u.nome, u.cognome, u.dataNascita> !є DB Redirect2.htm OK TSRC2 u.nick !є DB && tupla <u.nome, u.cognome, u.dataNascita> є DB ERRORIACCESSO.jsp OK TSRC3 u.nick є DB ERRORIACCESSO.jsp OK Servlet ServSA /*inizio*/ public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException { /*1*/ HttpSession sessione=request.getSession(true); ServletContext sc=getServletContext(); String Tipo=request.getParameter("D3"); String Pedina=request.getParameter("D4"); int NumGiocatori=Integer.parseInt(request.getParameter("D5")); String Nome=request.getParameter("D1"); String Ora=request.getParameter("D6")+":"+request.getParameter("D10")+" "+request.getParameter("D11"); String Data=request.getParameter("D7")+"/"+request.getParameter("D8")+"/"+request.getPa rameter("D9"); String address = null; Utente u=(Utente)sessione.getAttribute("utente"); sessione.setAttribute("pedina",Pedina); /*2*/ if ( Tipo.equals("Crea")) { /*3*/ Partita p=new Partita(Nome); Semaphore sem= new Semaphore(0); sem.release(); int count=NumGiocatori-1; String c2=ServletUtil.codSem(p); String c1=ServletUtil.codifica(p); String cod=ServletUtil.codifica(p,Pedina); /*4*/ if(!ServletUtil.controlloCreazione(cod, sc)){ /*5*/ Contesto con=new Contesto(p,0,u,NumGiocatori); sessione.setAttribute("game",p); sc.setAttribute(cod,con); sc.setAttribute(c2,sem); sc.setAttribute(c1, count); address ="Redirect7.htm"; } else{ /*6*/ sessione.setAttribute("err","Partita già esistente"); address="ERRORE.jsp"; } } /*7*/ else if (Tipo.equals("Partecipa")){ /*8*/ Partita p =new Partita(Nome,Data,Ora); String c2P=ServletUtil.codSem(p); String c1P=ServletUtil.codifica(p); /*9*/ if(ServletUtil.controlloPartecipazione(c2P,sc)&& ServletUtil.controlloPartecipazione(c1P,sc)){ /*10*/ Semaphore semp; semp=(Semaphore)sc.getAttribute(c2P); semp.acquireUninterruptibly(); int count=(Integer) sc.getAttribute(c1P); /*11*/ if(count!=0){ /*12*/ count=count-1; String cod=ServletUtil.codifica(p,Pedina); /*13*/ if(ServletUtil.controlloCreazione(cod, sc)){ /*14*/ if(!ServletUtil.controlloPartecipazione(cod, sc)){ /*15*/ address ="Redirect11.htm"; sessione.setAttribute("game",p); Contesto con=new Contesto(p,0,u); sc.setAttribute(cod,con); sc.setAttribute(c1P, count); semp.release(); } else{ /*16*/ sessione.setAttribute("err","Pedina in uso"); address="ERRORE.jsp"; semp.release(); } } else{ /*17*/ sessione.setAttribute("err","Partita non presente"); address="ERRORE.jsp"; semp.release(); } } else{ /*18*/ sessione.setAttribute("err","Partita Completa"); address="ERRORE.jsp"; semp.release(); } } else{ /*19*/ sessione.setAttribute("err","la partita Non Esiste"); address="ERRORE.jsp"; } } else{ /*20*/ Partita p =new Partita(Nome,Data,Ora); try { /*21*/ if(ControllerPartita.partitaPresente(p)&&ServletUtil.verificaLogati(sc, sessione, p)){ /*22*/ sessione.setAttribute("game", p); Utente u2=(Utente)sessione.getAttribute("utente"); String cod=ServletUtil.codificaRip2(p); /*23*/ if(!ServletUtil.controlloPartecipazione(cod, sc)){ /*24*/ LinkedList<String> list=new LinkedList<String>(); list.add(u2.getnick()); String c2=ServletUtil.codificaRip(p); sc.setAttribute(c2, 0); sc.setAttribute(cod, list); address="Redirect12.htm"; } else{ /*25*/ LinkedList<String> list=(LinkedList<String>) sc.getAttribute(cod); list.add(u2.getnick()); address="Redirect15.htm"; } } else{ /*26*/ sessione.setAttribute("err","la partita Non Esiste o non vi puoi partecipare"); address="ERRORE.jsp"; } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /*27*/ RequestDispatcher dispatcher=request.getRequestDispatcher(address); dispatcher.forward(request, response); } Grafo: Complessità Ciclomatica: V(G)=10 Cammini Ricoprenti: 2. C1: a-b1-c1-d1-f1 3. C2: a-b1-c1-e1-g1 4. C3: a-b-c2-d2-e2-f2-g2-h2-i2-l2-m2 5. C4: a-b-c2-d2-e2-f2-g2-h2-i2-u2-n2 6. C5: a-b-c2-d2-e2-f2-g2-h2-o2-p2 7. C6: a-b-c2-d2-e2-f2-q2-r2 8. C7: a-b-c2-d2-s2-t2 9. C8: a-b-c3-d3-e3-f3-g3-h3 10. C9: a-b-c3-d3-e3-f3-i3-l3 11. C10: a-b-c3-d3-m3-n3 Matrice d'incidenza: a b b1 c1 c2 c3 d1 d2 d3 e1 e2 e3 f1 f2 f3 g1 g2 g3 h2 h3 i2 i3 l2 l3 m2 m3 n2 n3 o2 p2 q2 r2 s2 t2 u2 C1 X X X C2 X X X C3 X X X X X X X X X C4 X X X X X X X X X C5 X X X X X X X X C6 X X X X X X C7 X X X X C8 X X X X X X C9 X X X X X X C10 X X X X coper to X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X Casi Testati: ID Input Output Esito TSSAC1 Tipo = “Crea”, Partita.p !є al contesto Redirect7.htm OK TSSAC2 Tipo = “Crea”, Partita.p є al contesto ERRORE.jsp OK TSSAC3 Tipo = “Partecipa”, (semaphore && count) є al contesto, count != 0, Partita p є contesto, Redirect11.htm pedina !є contesto OK TSSAC4 Tipo = “Partecipa”, (semaphore && count) є al contesto, count != 0, Partita p є contesto, ERRORE.jsp pedina є contesto OK TSSAC5 Tipo = “Partecipa”, (semaphore && count) є al contesto, count != 0, Partita p !є contesto ERRORE.jsp OK TSSAC6 Tipo = “Partecipa”, (semaphore && count) є al contesto, count = 0 ERRORE.jsp OK TSSAC7 Tipo = “Partecipa”, (semaphore && count) !є al contesto ERRORE.jsp OK TSSAC8 Tipo != “Crea” || “Partecipa”, (Partita p є DB && (Utente.nome,Partita p) є DB) == true, Partita.p !є contesto Redirect12.htm OK TSSAC9 Tipo != “Crea” || “Partecipa”, (Partita p є DB && (Utente.nome,Partita p) є DB) == true, Partita.p є contesto Redirect15.htm OK TSSAC10 Tipo != “Crea” || “Partecipa”, (Partita p є DB && (Utente.nome,Partita p) є DB) == false ERRORE.jsp OK 2.4 Note Cactus Difficile installazione nell’ambiente di lavoro (Eclipse), ci ha portato ad un ritardo considerevole nella consegna del progetto