Riprendiamo il nostro esempio
Riprendiamo da dove abbiamo lasciato in sospeso il mese precedente [1], ossia il testing del calcolo delle spese di spedizione. Vi ricordiamo al link [2] trovate il codice completo descritto in questa serie di articoli.
Il senso delle modifiche
Per riprendere l’esempio, iniziamo modificando l’ultimo test che abbiamo scritto: “should return true if valid params are passed”.
Quando si scrive software utilizzando TDD bisogna sempre ricordarsi che stiamo scrivendo solo il codice necessario a soddisfare il test, quindi l’unica modifica che dobbiamo applicare è quella che ci permette di avere un costo di € 27,00 in risposta alla nostra funzione. Per il calcolo, rimandiamo alle due tabelle con le condizioni specificate nel precedente articolo [1].
Modificare i test
Aggiungiamo pertanto la seguente riga:
expect(result).to.have.property('shippingFees', 27.0);
Lanciando i test dovremmo trovarci nuovamente in “red light”. Provocatoriamente andiamo in “green light” con la minima modifica al codice, ossia facciamo restituire il valore richiesto dal test.
return { requestValid: true, shippingFees: 27.0, message: '' };
Implementiamo i test successivi
Ora che abbiamo soddisfatto il primo test, iniziamo a implementare i test successivi, come, per esempio, la variazione di peso e di volume dei prodotti:
it('should returns correct cost with other valid params passed', () => { const params = { products: [ { code: 'ART1', quantity: 1, pricePerUnit: 34.5, volume: 4000000, weight: 200 }, { code: 'ART2', quantity: 1, pricePerUnit: 89.90, volume: 25000000, weight: 120 }, ], courier: 'DHL', courierService: 'Standard', destination: 'ITA', }; const result = ShippingService.getShippingFees(params); expect(result).to.have.property('requestValid', true); expect(result).to.have.property('shippingFees', 10.0); expect(result).to.have.property('message').to.be.empty; });
Lanciando il test: ovviamente quest’ultimo fallirà (Red–Green–Refactor). Chiaramente potremmo continuare all’infinito ad apportare modifiche “banali” al codice per continuare a soddisfare tutti i test, ma ora è giunto il momento di implementare una soluzione seria che ci permetta di soddisfare entrambi i test.
Tabelle come database
Visto lo scopo di questi articoli, non implementeremo una soluzione con un database e i suoi relativi servizi di accesso ma, per rimanere focalizzati sui test e sulla loro implementazione, creeremo due semplici tabelle di lookup con la trasposizione di quanto illustrato nell’articolo del mese scorso.
All’interno della cartella “src” creiamo una sottocartella chiamata “models” e al suo interno un nuovo file denominato “couriers.js” in cui inseriamo i modelli delle nostre tabelle prezzi.
const couriers = [ { courier: "DHL", weights: [ { min: 0, max: 2, price: 5 }, { min: 2, max: 5, price: 10 }, { min: 5, max: 9999999, price: 20 } ], volumes: [ { min: 0, max: 3, price: 5.5 }, { min: 3, max: 7, price: 8 }, { min: 7, max: 9999999, price: 27 } ] }, { courier: "UPS", weights: [ { min: 0, max: 2, price: 6.5 }, { min: 2, max: 5, price: 9 }, { min: 5, max: 9999999, price: 21 } ], volumes: [ { min: 0, max: 3, price: 6 }, { min: 3, max: 7, price: 11 }, { min: 7, max: 9999999, price: 30 } ] }, { courier: "FedEx", weights: [ { min: 0, max: 2, price: 7 }, { min: 2, max: 5, price: 12 }, { min: 5, max: 9999999, price: 24 } ], volume:[ { min: 0, max: 3, price: 5 }, { min: 3, max: 7, price: 10 }, { min: 7, max: 9999999, price: 29 } ] } ];
Ancora modifiche
Ora che abbiamo preparato l’ambiente, possiamo implementare le modifiche necessarie al nostro codice; in questa fase è fondamentale sottolineare che, trattandosi di refactoring, non andremo a scrivere nuovi test ma, come dice il nome, perfezioneremo il codice già esistente.
Questa è proprio la simulazione di un caso reale di manutenzione del software: ci troviamo nella condizione di dover modificare del codice funzionante, ma questa volta con la tranquillità che tutto quello che andremo a scrivere sia sotto la copertura dei test che dovranno tornare “verdi” al termine del lavoro.
Meno banale di quel che sembra
Per chi si affaccia al TDD per la prima volta, potrebbe sembrare un concetto banale, ma non lo è affatto. Basti pensare a quante volte è capitato di modificare del codice per scoprire a posteriori che altre parti del programma ne erano influenzate, facendoci perdere molto tempo in debug alla ricerca della causa.
Il passaggio naturale, a questo punto, sarebbe quello di implementare il codice necessario nella funzione shippingService in modo da soddisfare i test già implementati con l’utilizzo dei modelli creati poco sopra.
Una volta terminata questa fase, ci troveremmo nuovamente a fare refactoring del codice, per pulire/ottimizzare la funzione shippingService. Forti delle nozioni acquisite, sappiamo che prima dobbiamo scrivere i test, poi il codice necessario.
Per motivi di sintesi passeremo direttamente alla fase di refactoring, quindi alla scrittura dei nuovi test per la suddivisione del codice in piccoli metodi. Spezzare il codice in metodi più piccoli, ognuno con una sola responsabilità, ci permette di testarli meglio oltre che scrivere codice più leggibile e mantenibile. A questo proposito suggeriamo la lettura del libro di “Uncle Bob”, Clean Code [3].
Descriviamo i test
Vediamo di descrivere i test che abbiamo implementato partendo dalle regole che dobbiamo soddisfare.
Verificare i parametri
La prima riguarda la consegna standard e prevede in ingresso una serie di parametri che dobbiamo verificare che ci siano e siano corretti.
if (typeof params === 'undefined') { return { requestValid: false, shippingFees: 0, message: 'Specificare i parametri della richiesta' }; } if (typeof params.products === 'undefined' || params.products.length === 0) { return { requestValid: false, shippingFees: 0, message: 'Specificare l'elenco dei prodotti' }; } if (typeof params.courier === 'undefined' || params.courier === '') { return { requestValid: false, shippingFees: 0, message: 'Specificare il corriere' }; } if (typeof params.courierService === 'undefined' || params.courierService === '') { return { requestValid: false, shippingFees: 0, message: 'Specificare il servizio del corriere' }; } if (typeof params.destination === 'undefined' || params.destination === '') { return { requestValid: false, shippingFees: 0, message: 'Specificare la destinazione di spedizione' }; }
Inutile ricordare che è buona norma controllare sempre i parametri all’ingresso di ogni strato della nostra applicazione.
Regola per il costo
Una volta verificati i parametri, possiamo iniziare a implementare la prima regola riguardante il costo per un dato corriere. Iniziamo verificando se il corriere specificato è disponibile nel nostro database (courier.js) e, nel caso, ci facciamo restituire le sue proprietà.
//1. Looking for Courier let courierParams = getCourierInfo(params.courier); if (courierParams === undefined) { return { requestValid: false, shippingFees: 0, message: 'Corriere non identificato' }; }
Verificato il corriere è la volta del calcolo del costo standard di spedizione in base al peso volumetrico e/o al peso degli articoli presenti nel carrello:
//2. Compute StandardShippingFees let shippingFees = getStandardShippingFees(params.products, courierParams.weights, courierParams.volumes);
È importante sottolineare che, sebbene non abbiamo ancora fatto nulla di particolarmente complicato, il codice che stiamo implementando è ben suddiviso per responsabilità.
Fare refactoring su un codice simile non sarà complicato, e soprattutto, una modifica al servizio che restituisce i parametri del corriere non comporterà modifiche al servizio del calcolo del costo di spedizione.
Il codice esposto sopra soddisfa la prima regola, mentre lasciamo a voi, come esercizio, la stesura dei test e relativo codice per soddisfare le rimanenti regole.
Conclusioni
È giunta l’ora di trarre delle conclusioni a questa lunga chiacchierata sull’importanza dei Test. Abbiamo già scritto che scrivere test non è un’attività gratuita, quindi, visto che nessuno di noi ha del tempo extra da investire sui propri progetti, deve esserci una più che valida giustificazione.
Abbiamo cercato di dimostrare, più che raccontare, perché il tempo extra investito nello scrivere test sia, in realtà, tempo risparmiato. All’inizio di ogni progetto, semplice o difficile che esso sia, è “facile” per lo sviluppatore esperto districarsi fra le specifiche del cliente e scrivere del codice tutto sommato pulito e funzionante.
Vi possiamo garantire che, se il ciclo di vita dell’applicazione finisse qui, con la prima consegna, scrivere test sarebbe solo un’inutile perdita di tempo. Ma sappiamo tutti che il primo rilascio è solo l’inizio di un lungo viaggio — o almeno ce lo auguriamo — e, man mano che il progetto cresce, diventa sempre più difficile apportare modifiche che non siano invasive.
Magari non lo scopriamo subito che un battito d’ali di farfalla provocherà un uragano… ma lo farà! Allora perché non cercare di prevenirlo questo uragano? Perché non dotare il nostro software di quelle misure che ci metteranno al riparo dalle conseguenze di questa tempesta?
I test non sono l’unico modo, anzi i test sono solo lo strumento che ci permette di scrivere del codice “pulito”. Ci obbligano a pensare prima di scrivere, e già questo non sarebbe poco. Ma ci indirizzano anche nello scrivere classi dedicate, a rispettare il Single Responsibility Principle, che come ci insegna l’onnipresente Uncle Bob, facilita la manutenzione del software una volta rilasciato. Vuoi mettere la tranquillità con cui si modifica una classe che fa una sola cosa? Magari sotto la copertura del test? Che problema c’è? Nessuno, direte voi. “Appunto”, rispondiamo noi.
Nell’esempio associato a questi articoli trovate alcuni test d’esempio; sono tutti Green, tornando al mantra del TDD, ma molti altri se ne possono implementare, o semplicemente, si possono modificare le regole, come spesso capita nella vita reale di un software, e far saltare tutti test, andare in Red status. Ma vi accorgerete com’è facile tornare ad una situazione di Green, perché il refactor del codice ora è coperto dai test, che ci dicono in tempo reale, se le modifiche che stiamo apportando forniscono i risultati desiderati.
Speriamo di aver stuzzicato la vostra curiosità, quindi buon test a tutti.