Guida Test Driven Development – Eseguiamo questa tecnica di programmazione per sviluppare funzionalità della nostra piattaforma per la creazione di chatbot crafter.ai.

In questo articolo vogliamo spiegare il perché dovreste usare questa tecnica di programmazione per i vostri prodotti o servizi.

Definizione di test automatico

Un test automatico è uno script che ha il compito di verificare il comportamento di un dato frammento di codice.

In questo articolo non ci dilungheremo sui vari tipi di test ma lavoreremo creando dei test unitari che andranno a validare la nostra funzione. Utilizzeremo la libreria unittest di python già inclusa tra quelle standard del linguaggio.

Definizione del Test Driven Development

Il Test Driven Development (TDD) è una tecnica di sviluppo software in cui vengono prima scritti i test e successivamente il codice che li soddisfa.

Vantaggi del Test Driven Development

Codice coperto da test

Questa procedura consente di avere sempre un codice ben coperto da test e di conseguenza più manutenibile.

Focus sull’obiettivo

I test scritti hanno il compito di andare a coprire tutte le richieste, quando andremo quindi a scrivere il codice abbiamo già definito di preciso il suo comportamento.

Documentazione

Sviluppando i test sulle funzionalità richieste avremo che i test stessi faranno da documentazione al codice. Inoltre i test vengono aggiornati durante la modifica del codice eliminando il problema dell’aggiornamento della documentazione.

Esempio di Test Driven Development

In questo primo articolo voglio portare subito con un esempio di Test Driven Development partendo dal FizzBuzz Kata

Criteri di accettazione

Per il testo completo del kata vi rimando al link indicato sopra. Per praticità riporto i criteri d’accettazione che deve avere il nostro metodo per superare la prima parte dell’esercizio:

  1. se il numero passato in input non è multiplo di 3 e/o 5 allora la funzione dovrà restituire quel numero
  2. se il numero passato in input è un multiplo di 3 allora la funzione tornerà la stringa ‘fizz’
  3. se il numero in input è multiplo di 5 la funzione tornerà la stringa ‘buzz’
  4. se il numero in input è multiplo di 3 e 5 la funzione tornerà ‘fizzbuzz’

1. I numeri non divisibili per 3 e/o 5

Per prima cosa definiamo la funzione say_number senza nessun comportamento e il primo test richiesto.

import unittest

def say_number(number):
    pass

class TestSayNumber(unittest.TestCase):
    def test_return_number_string(self):
        self.assertEqual(say_number(1), '1')
        self.assertEqual(say_number(2), '2')
        self.assertEqual(say_number(11), '11')
        self.assertEqual(say_number(13), '13')


if __name__ == '__main__':
    unittest.main()

Eseguendolo il test si rompe, andiamo quindi a scrivere il codice che lo soddisfa

def say_number(number):
    return str(number)

Lanciando i test possiamo vedere che il nostro codice li supera.

Note

Il numero di asserzioni che andiamo a fare è limitato, deve essere un numero ragionevole da poter coprire un buon numero di casi ma non potranno mai essere coperti tutti. E possibile prevedere dei test con dei numeri randomici ma questo complicherebbe di molto la scrittura dei test, occupatevi di eventuali problemi solo dopo che si presentano!

2. Multipli di tre

Andiamo quindi ad aggiungere un nuovo metodo alla nostra classe di test

def test_return_fizz(self):
    self.assertEqual(say_number(3), 'fizz')

procediamo per stadi aggiungendo solo il controllo sul numero tre e andiamo a soddisfare il nostro test

def say_number(number):
    if number == 3:
        return 'fizz'
    return str(number)

Naturalmente il codice soddisfa il test ma non soddisfa la richiesta. Nel mondo reale un comportamento di questo tipo andrebbe a generare un bug. Andiamo ad esempio a creare un semplice script che va ad utilizzare il nostro codice:

for number in range(100):
    print(say_number(number))ber(number))

eseguendolo otteniamo:

1
2
fizz
4
5 # caso non ancora gestito
6 # ERRORE

Abbiamo quindi trovato un bug, cosa fare? Aggiungiamolo ai nostri test!

def test_return_fizz(self):
    self.assertEqual(say_number(3), 'fizz')
    self.assertEqual(say_number(6), 'fizz')

Ricostruendo il bug tramite un test automatico, sappiamo che superando il test risolveremo il bug

def say_number(number):
    if number == 3 or number == 6:
        return 'fizz'
    return str(number)

Lanciamo i test e anche questa volta vengono superati. Per sicurezza però andiamo ad aggiungere nuove asserzioni:

def test_return_fizz(self):
    self.assertEqual(say_number(3), 'fizz')
    self.assertEqual(say_number(6), 'fizz')
    self.assertEqual(say_number(9), 'fizz')
    self.assertEqual(say_number(12), 'fizz')

Ci rendiamo conto che c’è qualcosa che non va, dobbiamo cercare una strategia migliore ed evitare di continuare a gestire ogni singolo multiplo di 3

def say_number(number):
    if (number % 3) == 0:
        return 'fizz'
    return str(number)

Perfetto, ora anche eseguendo lo script che usa la nostra funzione possiamo vedere che tutti i multipli di 3 sono gestiti.

3. Multipli di 5

Passiamo ai multipli di 5 quindi e scriviamo come sempre prima i test:

def test_return_buzz(self):
    self.assertEqual(say_number(5), 'buzz')
    self.assertEqual(say_number(10), 'buzz')
    self.assertEqual(say_number(20), 'buzz')
    self.assertEqual(say_number(35), 'buzz')

e poi aggiorniamo la nostra funzione:

def say_number(number):
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
            return 'buzz'
    return str(number)

4. Multipli di 3 e 5

passiamo all’ultimo punto delle nostre richieste come sempre andiamo a scrivere i test:

def test_return_fizzbuzz(self):
    self.assertEqual(say_number(15), 'fizzbuzz')
    self.assertEqual(say_number(30), 'fizzbuzz')
    self.assertEqual(say_number(60), 'fizzbuzz')

e il nostro codice:

def say_number(number):
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
        return 'buzz'
    if (number % 3) == 0 and (number % 5) == 0:
        return 'fizzbuzz'
    return str(number)

ora però eseguendo i test vediamo subito che il nostro codice non li supera, infatti say_number(15) restituisce “fizz” e non “fizzbuzz”. Analizzando il codice ci rendiamo subito conto che essendo divisibile per 3 entriamo nel primo if e non arriviamo mai al terzo. Per risolvere questo problema basta spostare semplicemente l’ultimo if al primo posto

def say_number(number):
    if (number % 3) == 0 and (number % 5) == 0:
        return 'fizzbuzz'
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
            return 'buzz'
    return str(number)

Ora superiamo i test. I più attenti potranno notare che il primo if può essere riscritto visto che se un numero è divisibile per 3 e 5 sarà sicuramente un multiplo di 15. Effettuiamo questa piccola rifattorizzazione:

def say_number(number):
    if (number % 15) == 0:
        return 'fizzbuzz'
    if (number % 3) == 0:
        return 'fizz'
    if (number % 5) == 0:
            return 'buzz'
    return str(number)

I test andranno ad assicurarci che il cambiamento effettuato è corretto e che il comportamento del nostro metodo continua ad essere lo stesso per i valori per cui lo stiamo testando.

Conclusioni – guida Test Driven Development

Naturalmente il codice potrebbe essere rifattorizzato e scritto meglio ma questo voleva essere solo un esempio per prendere dimestichezza con il metodo di sviluppo.

Successivamente continueremo il kata implementando il resto delle richieste ed effettuando una riscrittura del codice.

E come mi disse un saggio senior quando stavo iniziando ad utilizzare il Test Driven Development:

non importa quando testi, l’importante è che testi” – cit Matteo Galacci