In questo articolo proveremo a realizzare un piccolo progetto adottando la metodologia Test Driven.
Lavoriamo in ambiente Java e utilizziamo JUnit come framework per i nostri test.
Supponiamo di voler sviluppare l’infrastruttura di base per un progetto di Wallet elettronico, ovvero un portafoglio digitale che mi permetta di memorizzare conti e carte di credito.
Ecco in dettaglio alcuni dei requisiti o potenziali use-case a nostra disposizione:
- L’utente accede al proprio Wallet mediante login e password.
- Sono previsti due ruoli d’accesso principali al sistema: utente base o amministratore del sistema.
- L’utente associa una o più carte di pagamento al proprio Wallet.
- L’utente può associare anche un conto corrente o altri strumenti di pagamento.
- I pagamenti avvengono interfacciandosi, con un formato noto, ad un servizio di terze parti.
- Il Wallet non deve caricare immediatamente i dati sensibili relativi agli strumenti di pagamento. Questi ultimi devono essere recuperati esclusivamente quando servono.
- L’utente, una volta all’interno del suo Wallet, potrà visualizzare l’elenco delle carte e dei conti inseriti.
- L’utente deve poter inserire dei beneficiari per poter trasferire del denaro.
- i beneficiari devono avere anche loro un wallet elettronico di appoggio.
- Un wallet è identificato da un codice alfa-numerico univoco.
Ecco quindi la possibile roadmap di test da eseguire:
- Test base su login e profilazione.
- Test base sul Wallet: creazione e aggiunta degli strumenti di pagamento.
- Test di caricamento del Wallet.
- Test per le operazioni di pagamento con gli strumenti presenti nel Wallet.
Test per login e profilazione
Partiamo quindi dai primi due requisiti e poniamoci la domanda “quali test posso immaginare per questi use-case?”
La profilazione mediante login direi che è stata già affronta nel mio precedente articolo, quindi potremmo partire da quell’esempio tanto per non buttare via nulla.
Riassumiamo brevemente. Volevamo simulare l’accesso ad un’area riservata inserendo utente e password come credenziali ed ottenendo in uscita il ruolo e la pagina di nostra competenza.
Avevamo creato un POJO per i dati utente:
}
Ho aggiunto al mio utente un oggetto WalletID che identifica univocamente il suo portafoglio elettronico.
Una classe Mock per simulare i dati sul mio database:
}
Una classe Manager per gestire il controllo di accesso in base ai ruoli utente:
}
Ho modificato quest’ultima classe in modo da gestire solo due ruoli, amministratore e utente standard così come previsto dal requisito 2.
>> Refactoring: naturalmente potremmo discutere di come fare il refactoring introducendo una classe Role che mi permetta di gestire quanti ruoli voglio in maniera dinamica, ma al momento atteniamoci alle specifiche in nostro possesso e prendiamo per buona la soluzione poco elegante gestita con le costanti stringa.
Con queste classi così composte abbiamo creato una semplice suite di test per verificare i tre casi possibili di accesso: amministratore, utente e login errata.
La nostra classe manager restituisce il nome della pagina di competenza mentre il ruolo dell’utente e il WalletID vengono letti dal nostro database e memorizzati nella classe pojo di competenza (User).
Assumiamo che il WalletID venga utilizzato, in seguito, per caricare effettivamente i dati del portafoglio elettronico (requisiti 6 e 10). Poichè voglio effettuare dei test già in fase di login sul corretto caricamento di questo oggetto e dato che solo io posso decidere le regole che rendono due WalletID identici, occorre effettuare l’override del metodo equals così da metterci al sicuro per i futuri controlli di uguaglianza:
}
Ecco ad esempio il test per la login da amministratore:
}
Con questi test controllo che l’amministratore abbia sempre un portafoglio associato, che questo è sempre diverso da null, che è sempre uguale a quello atteso e che è sempre diverso da un altro ipotetico. Un discorso analogo vale anche per i test di login utente base.
In questo modo abbiamo già implementato il codice per i primi due requisiti facendoci guidare dai nostri test. Una volta verificato che il modulo di login funziona e restuisce il corretto WalletID in base al ruolo dell’utente e alle credenziali inserite, possiamo passare oltre.
Test di base sul Wallet: creazione e aggiunta degli strumenti di pagamento
Dato che dobbiamo gestire un portafoglio elettronico avrò bisogno di una classe principale che lo rappresenti e dato che la mia lista dei requisiti parla di inserire strumenti di pagamento e beneficiari, un buon punto di partenza potrebbe essere il seguente:
Stando ai requisiti 3 e 4 devo poter gestire un elenco non omogeneo di strumenti di pagamento: carte di credito e conti correnti. Successivamente potrei anche voler gestire delle pre-pagate e chissà quali altri strumenti di pagamento.
>> Design: l’interfaccia PaymentTool mi consente di gestire dignitosamente questa situazione. Sarei potuto partire con due classi base CreditCard e BankAccount e poi fare successivamente il refactoring per passare all’interfaccia, ma questo approccio non ha senso se si ha già una buona idea in mente. E’ naturale che al crescere dell’esperienza saremo in grado di impostare il progetto in modo ben strutturato e quindi il refactoring non sempre rientrerà tra le nostre attività.
La mia interfaccia espone un solo metodo che verrà utilizzato per estrarre i dati nel formato previsto dal sistema di pagamento al quale mi dovrò interfacciare. Semplifichiamo supponendo che il formato sia una banale stringa di campi separati dal carattere pipe “|” :
In questo modo mi porto a casa anche il requisito 5.
L’oggetto PaymentRecipient è invece una semplice classe pojo che, per semplicità, mi permette di memorizzare il nome utente e il suo WalletID così da poterli successivamente visualizzare in un elenco e chissà cosa altro. Questo pone le basi per i requisiti 8 e 9.
Passiamo quindi ai primi test sull’oggetto DigitalWallet. Cominciamo col caricarne uno vuoto e effettuare su di esso l’operazioni di base addTool che aggiunge un nuovo strumento di pagamento.
Decido che un nuovo PaymentTool può essere inserito nel DigitalWallet solo se non è già presente e per effettuare questo tipo di controlli implemento alcuni metodi a contorno: findTool e canAdd.
Ecco quindi la classe di test per il mio DigitalWallet:
Affinchè tutto vada liscio occorre ricordarsi dei metodi equals. Come già detto infatti, solo io posso decidere le regole che rendono uguali i miei PaymentTool ragion per cui:
}
Una volta sicuri che la funzionalità di base per l’inserimento dei tool di pagamento è testata e funzionante, possiamo passare all’integrazione con il database e quindi all’operazione di loading di un wallet pre-esistente. Questo per validare anche i requisiti 6 e 10.
Test per il caricamento dei dati del Wallet
Prima di tutto devo poter caricare il wallet a partire dal suo WalletID e eventualmente visualizzarne il suo contenuto. L’operazione di loading effettua il caricamento dei dati a partire dal mio database.
Predisponiamo quindi la classe MockDAO affinchè sia in grado di caricare un wallet.
>> Refactoring: anche in questo caso, passatemi la semplificazione. Dovremmo implementare classi DAO differenti per differenti tipi di dati. Dovremmo quindi creare una classe MockWalletDAO e eventualmente convertire quella esistente in MockUserDAO, tuttavia ai fini del nostro esempio questo è del tutto irrilevante e lo lascio come spunto per un refactoring interessante del nostro codice.
Con queste modifiche, il MockDAO mette a disposizione due utenti di prova e un wallet con due strumenti di pagamento: una carta di credito e un conto corrente. Ecco in dettaglio il mio costruttore:
Adesso che il nostro DAO è in grado di recuperare i dati del nostro Wallet non resta che la classe di test:
>> Refactoring: il metodo loadData mi permette di validare il requisito 6. Potremmo pensare di implementare un pattern VirtualProxy in modo da gestire in maniera ancor più elegante la nostra operazione di caricamento.
Test di pagamento con gli strumenti presenti nel Wallet
A questo punto possiamo passare all’ultimo step che avevamo previsto, ovvero un test per verificare la correttezza delle operazioni di pagamento.
Decido che il mio Wallet avrà uno strumento di pagamento di default che l’utente potrà scegliere tra quelli inseriti. Automaticamente il primo PaymentTool diventa lo strumento di default ed eventualmente mi preoccuperò successivamente di capire come e quando far cambiare questa impostazione all’utente. Il tool di default viene salvato in un’apposita variabile del DigitalWallet in modo da avere un’accesso rapido all’oggetto.
Applico quindi una piccola modifica al metodo addTool del DigitalWallet:
Implemento due metodi necessari per chiudere il cerchio:
public boolean doPayment() {
Con queste modifiche alla mano posso già passare alla mia classe di test alla quale aggiungo:
Questo test presuppone che il metodo useForPayment dei vari PaymentTool funzioni nel modo atteso. Inutile dire che occorre la classe di test per rassicurarci anche su questo:
Infine aggiungo questa classe alla mia TestSuite in modo da poter eseguire tutti i test con un solo click:
>> Refactoring e Design: ovviamente non tutto quello che abbiamo visto è elegante, completo o correttamente implementato. Mancano controlli di sicurezza e validazione, mancano la parte sui beneficiari e quella per la visualizzazione e chissà quante altre cose. Un buon esercizio potrebbe essere quello di implementare i pezzi mancanti in modo da colmare questo vuoto e prendere dimestichezza con la metodologia.
Conclusioni
Cosa succede adesso?
Abbiamo implementato alcuni metodi di base per il nostro DigitalWallet e verificato che funzionano. Il passo successivo sarebbe quello di implementare una semplice applicazione Web che permette all’utente di interagire con il suo portafoglio elettronico.
In sintesi:
- Connessione reale al database.
- Pagina di login.
- Pagina di accesso profilato con le funzionalità base di aggiunta strumenti e pagamento usando il DigitalWallet.
Il nostro tutorial termina qui. Spero di aver eliminato un pò del fumo che sta intorno alla metodologia test driven e di aver trasmesso correttamente il tipo di approccio da utilizzare.
Per quelli più interessati è possibile scaricare i sorgenti del progettino di cui abbiamo discusso.