Test Driven Development – Primi passi

Eccoci al secondo articolo sul TDD. Spero di avervi incuriosito nell’introduzione precedente. É arrivata l’ora di passare agli aspetti pratici.

Premessa

La tecnica del TDD è applicabile con diversi linguaggi, tecnologie e Framework. In questa serie di articoli useremo principalmente il linguaggio di programmazione JAVA. Vedremo l’utilizzo del TDD con diversi Framework e paradigmi di programmazione (orientata agli oggetti, orientata agli aspetti).

Prerequisiti per la prima lezione

Conoscenza dei fondamenti di programmazione a oggetti.

Superficiale conoscenza del linguaggio di programmazione JAVA (versione 7).

Test Driven Development

Ciclo di sviluppo

Nel primo articolo, abbiamo indicato qual è il ciclo di sviluppo utilizzando la tecnica del TDD:

Ciclo sviluppo TDD
Ciclo sviluppo TDD

Il nostro ciclo di sviluppo implica che dobbiamo prima di tutto scrivere un test (che fallirà), scrivere il minimo indispensabile codice di produzione per eseguire il test. Se il test fallisce, scrivere altro codice. Altrimenti si può passare a scrivere un altro test. Opzionalmente si può effettuare il refactoring del codice in modo da togliere codice duplicato, rendere più leggibile il codice, …

Strumenti necessari

xUnit Framework

Kent Back, universalmente riconosciuto come il creatore dell’Extreme Programming e del TDD, ha per primo progettato un Testing Framework (SUnit per Smalltalk). I Testing Framework consentono di testare svariate entità di un sistema software. Una parte di questi framework fa parte di un gruppo definito come xUnit Framework. Come è intuibile dal nome, essi permettono di effettuare degli Unit Test: cioé test di una singola unità software. Nella programmazione ad oggetti, per singola unità, intendiamo una singola classe o un singolo metodo.

La x del termine xUnit viene sostituita con l’iniziale del linguaggio a cui il Testing Framework fa riferimento: JUnit per Java.

 

Junit

Junit è stato creato da Kent Back e Erich Gamma. In questa serie di articoli faremo riferimento alla versione 4 definita nel package org.junit.

Eploreremo questa libreria nel corso degli articoli, evitando di dilungarci troppo ora.

IDE

Non è strettamente necessario un IDE per seguire questi articoli. Sicuramente è comodo, sia per scrivere codice, ma soprattutto per gestire i test con un click. Se siete alla ricerca di un IDE agile e dedicato al refactoring, consiglio Idea: http://www.jetbrains.com/idea/.

Maven

É un build manager. Ci permette di gestire il progetto definendo le dipendenze e altre impostazioni in un file XML (pom.xml). Le dipendenze vengono automaticamente scaricate da repository remoti. Anche questo non è strettamente necessario per seguire gli articoli, almeno in questa prima fase, ma è sicuramente utile e proficuo usarlo per i propri progetti.

Primi passi

Fattorizzazione di un numero in fattori primi

Iniziamo a conoscere in pratica il TDD risolvendo un problema. Prendo a prestito questo esercizio da uno dei kata (ci torneremo sui kata) di Robert C. Martin.

Requisiti

Definire una classe chiamata PrimeFactors cha ha un metodo statico: generate. Il metodo generate ha come parametro un numero intero e restituisce una lista di fattori primi.

Iniziamo

Creiamo un progetto PrimeFactors. Se utilizziamo Maven dobbiamo aggiungere la dipendenza per Junit4:


    
        junit
        junit
        4.10
        test
    

Creiamo la nostra classe di TestCase (raggruppa i test per un classe o funzionalità):

PrimeFactorsTest.java

package primeFactors;

/**
 * Class: PrimeFactorsTest
 *
 * @version 1.0
 */
public class PrimeFactorsTest {
}

In Junit4 non è necessario estendere TestCase. Sarà sufficiente annotare ogni metodo come @Test.

Primo test

Cominciamo a scrivere il primo test. Vogliamo testare la fattorizzazione del numero 1.

PrimeFactorsTest.java

package primeFactors;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;

/**
 * Class: PrimeFactorsTest
 *
 * @version 1.0
 */
public class PrimeFactorsTest {

    @Test
    public void testOne() {
        List expectedFactors = new ArrayList<>();
        //we expect an empty list of factors
        assertEquals(expectedFactors, PrimeFactors.generate(1));
    }
}

L’annotazione @Test (org.junit.Test) sta ad indicare che il metodo è un test case.

Stiamo testando la fattorizzazione del numero 1. Ci aspettiamo che ritorni una lista vuota, non essendo un numero fattorizzabile.

assertEquals è un metodo statico di org.junit.Assert ed è il cuore del nostro test: indica ciò che ci aspettiamo, il risultato che vogliamo raggiungere. Richiediamo che la fattorizzazione del numero 1 ritorni una lista di interi vuota.

Se il nostro IDE è abbastanza intelligente, ancor prima di eseguire il test, ci viene segnalato che la classe PrimeFactors non esiste e ci viene proposto di crearla. Se non stiamo utilizzando un IDE, avremo un errore di compilazione che ci indicherà subito che non può essere trovato il simbolo PrimeFactors.

Non ci resta che creare la nostra classe PrimeFactors. L’IDE ci suggerirà di definire un metodo generate(int n).

PrimeFactors.java

package primeFactors;

import java.util.ArrayList;
import java.util.List;

/**
 * Class: PrimeFactors
 *
 * @version 1.0
 */
public class PrimeFactors {
    public static List generate(int n) {
        return new ArrayList<>();
    }
}

Eseguiamo il test:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running primeFactors.PrimeFactorsTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.058 sec

Ottimo, il primo unit test è passato. Non è necessario scrivere altro codice. Possiamo effettuare del refactoring prima di scrivere un altro test.

Dato che ci aspetteremo sempre una lista di interi, per evitare di dover definire una lista di interi per ogni test case, definiamo un metodo. Su Idea ci basta selezionare la riga in cui instanziamo la lista dei fattori primi che ci aspettiamo, premiamo CTRL+ALT+M e viene estratto un metodo.

PrimeFactorsTest.java

public class PrimeFactorsTest {

    @Test
    public void testOne() {
        //we expect an empty list of factors
        assertEquals(list(), PrimeFactors.generate(1));
    }

    private List list() {
        return new ArrayList<>();
    }
}

Rieseguiamo il test. Green! Possiamo procedere con il prossimo test.

Secondo test

PrimeFactorsTest.java:23

    @Test
    public void testTwo() {
        assertEquals(list(2), PrimeFactors.generate(2));
    }

2 ha un solo fattore primo: 2. Il metodo list(2) non esiste e l’IDE ce lo segnala. Dobbiamo correggere la definizione del metodo.

PrimeFactorsTest.java:28

    private List list(int... factors) {
        List list = new ArrayList<>();
        for (int i : factors)
            list.add(i);
        return list;
    }

La classe di PrimeFactorsTest diventa:

package primeFactors;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;

/**
 * Class: PrimeFactorsTest
 *
 * @version 1.0
 */
public class PrimeFactorsTest {

    @Test
    public void testOne() {
        //we expect an empty list of factors
        assertEquals(list(), PrimeFactors.generate(1));
    }

    @Test
    public void testTwo() {
        assertEquals(list(2), PrimeFactors.generate(2));
    }

    private List list(int... factors) {
        List list = new ArrayList<>();
        for (int i : factors)
            list.add(i);
        return list;
    }
}

Eseguiamo i test:

Running primeFactors.PrimeFactorsTest
Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.07 sec <<< FAILURE!

Results :

Failed tests:   testTwo(primeFactors.PrimeFactorsTest): expected:<[2]> but was:<[]>

Tests run: 2, Failures: 1, Errors: 0, Skipped: 0

Naturalmente il secondo test è fallito. Junit ci indica anche il motivo del fallimento: nel test case testTwo ci aspettavamo una lista contenente l’intero 2, ma è stata tornata una lista vuota. É necessario correggere il metodo generate(int n).

PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        if (n > 1) {
            primes.add(2);
        }
        return primes;
    }

Eseguiamo il test: Success!

Terzo test

Testiamo la fattorizzazione del numero 3 e ci aspettiamo un solo fattore primo: 3.

PrimeFactorsTest.java:28

    @Test
    public void testThree() {
        assertEquals(list(3), PrimeFactors.generate(3));
    }

Test fallito. Correggiamo il metodo generate(int n) della classe PrimeFactors.java:


PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        if (n > 1) {
            primes.add(n);
        }
        return primes;
    }


Non conosciamo un algoritmo corretto, ma man mano che proseguiamo con i test emergerà da solo.


Eseguiamo il test e il risultato sarà positivo. Non abbiamo nulla per cui effettuare il refactoring, scriviamo il prossimo test.

Quarto test

La fattorizzazione del numero 4 risulterà in due fattori identici: {2, 2}.

PrimeFactorsTest.java:33

    @Test
    public void testFour() {
        assertEquals(list(2, 2), PrimeFactors.generate(4));
    }

Eseguiamo il test:

Failed tests:   testFour(primeFactors.PrimeFactorsTest): expected:<[2, 2]> but was:<[4]>

PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        if (n > 1) {
            if (n%2 == 0) {
                primes.add(2);
                n /= 2;
            }
            if (n > 1)
                primes.add(n);
        }
        return primes;
    }


Per induzione, stabiliamo che un numero pari (divisibile per 2 quindi), ha almeno un fattore primo uguale a 2 e poi il resto della divisione per 2.


Eseguendo il test otteniamo un risultato positivo.

Quinto test

Proviamo con il prossimo numero  non primo: 6. Naturalmente ci aspettiamo due fattori primi: 2 e 3.

PrimeFactorsTest.java:38

    @Test
    public void testSix() {
        assertEquals(list(2, 3), PrimeFactors.generate(6));
    }

Il test passa. Bene.

Sesto test

Il prossimo numero non primo è l’8. Ci aspettiamo i seguenti fattori: 2 2 2.

PrimeFactorsTest.java:43

    @Test
    public void testSix() {
        assertEquals(list(2, 2, 2), PrimeFactors.generate(8));
    }

Il test fallisce. Se analizziamo l’algoritmo messo a punto finora, ci rendiamo conto che è necessaria questa modifica:


PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        if (n > 1) {
            while (n % 2 == 0) {
                primes.add(2);
                n /= 2;
            }
            if (n > 1)
                primes.add(n);
        }
        return primes;
    }


Abbiamo sostituito il precedente if ( n % 2 == 0 ) con un while.

Il test passa.

Settimo test

Siamo già arrivati a scrivere 7 test. Se ci facciamo caso, abbiamo scritto più linee di test che linee di codice di produzione. Adesso testiamo il nostro metodo con il numero 9 e attendiamo due fattori: 3 e 3. Già intuiamo che il test fallirà e il perché.
PrimeFactorsTest.java:48

    @Test
    public void testNine() {
        assertEquals(list(3, 3), PrimeFactors.generate(9));
    }

Il test fallisce. Dobbiamo lavorare sul codice del nostro metodo.

Riflettendo su ciò che abbiamo fatto finora, intuiamo che il numero 2 che utilizziamo nell’operazione modulo (riga 15: while ( n % 2 == 0)) rappresenta un fattore primo candidato.


PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        int candidate = 2;
        if (n > 1) {
            while (n % candidate == 0) {
                primes.add(candidate);
                n /= candidate;
            }
        }
        if (n > 1)
            primes.add(n);
        return primes;
    }


Introduciamo una variabile che conterrà il fattore primo candidato.


Verifichiamo, lanciando tutti i test, di non aver rotto nulla. Ok, fallisce solo l’ultimo test che abbiamo aggiunto. Senza gli unit test, come avreste potuto verificare di non aver introdotto bug con l’ultima modifica? Ok, adesso stiamo giocando su del codice semplice. Ma immaginate di lavorare in un progetto molto più complesso!

Proseguiamo con la nostra modifica.


PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        int candidate = 2;
        while (n > 1) {
            while (n % candidate == 0) {
                primes.add(candidate);
                n /= candidate;
            }
            candidate++;
        }
        if (n > 1)
            primes.add(n);
        return primes;
    }


Abbiamo aggiunto un ulteriore while, al posto di if ( n > 1) e abbiamo aggiunto una linea in cui incrementiamo la variabile candidate ad ogni ciclo del while più esterno.

Pare possa essere migliorato ulteriormente, ma prima eseguiamo il test e solo se il test ha successo possiamo effettuare il refactoring.


Il nostro settimo test passa.

Refactoring


PrimeFactors.java:12

   public static List generate(int n) {
        List primes = new ArrayList<>();
        int candidate = 2;
        while (n > 1) {
            for (; n % candidate == 0; n /= candidate) {
                primes.add(candidate);
            }
            candidate++;
        }
        return primes;


L’ultimo if è inutile. Ce ne rendiamo conto perché il while termina quando n non è più maggiore di 1 e quindi la condizione dell’ultimo if sarà sempre false. Possiamo eliminarlo.
Il while interno, può essere sostituto con un compatto for.



I test sono tutti green. Ok, non abbiamo rotto nulla.

PrimeFactors.java:12

    public static List generate(int n) {
        List primes = new ArrayList<>();
        for (int candidate = 2; n > 1; candidate++)
            for (; n % candidate == 0; n /= candidate)
                primes.add(candidate);
        return primes;


Anche l’if più esterno può essere sostituito con un compatto for.



Tutto funziona. L’algoritmo è di appena tre righe!

Conclusioni

Ho scelto di proposito questo esercizio, perché molto semplice dal punto di vista tecnico, in modo da evidenziare maggiormente gli aspetti del TDD e non distogliere con la parte tecnica.

É emersa immediatamente la brevità del ciclo test-codice. I test stessi ci hanno aiutato a trovare la soluzione per il nostro problema. Inoltre, la certezza di verificare eventuali introduzioni di bug con le nostro modifiche, ci ha permesso di avere il coraggio di effettuare un refactoring del codice migliorandone la leggibilità, l’eleganza e le performance.

Questo esempio è solo l’inizio del nostro piccolo viaggio. É servito a farci venire il languorino e la curiosità di vedere come sviluppare un’applicazione reale e complessa in TDD.

Chi ha letto fin qua, forse si starà chiedendo come è possibile sviluppare un’applicazione complessa che interargisce con un db o con un server, ad esempio, attraverso i test. Non solo è possibile, ma questo approccio di sviluppo ci permetterà di sviluppare la nostra applicazione nel migliore dei modi. Nel corso delle lezioni vedremo come il design pattern Dependency Injection è una diretta conseguenza del design guidato dai test.

Le prossime lezioni saranno più tecniche e permetteranno di realizzare un’applicazione complessa e utile.

Note sui Code Kata

L’esercizio che ho proposto, è un kata scritto da Robert C. Martin. I kata, nelle arti marziali giapponesi, indicano una serie di movimenti preordinari e codificati che rappresentano varie tecniche e tattiche di combattimento. Dave Thomas riprende il concetto di Kata dalle arti marziali e lo applica all’informatica. Si tratta di una serie di esercizi di programmazione che aiutano a sviluppare le proprie capacità attraverso la pratica e la ripetizione. Se ad una prima analisi questi esercizi (come quello svolto ora) possono apparire banali o inutili, ripeterli innumerevoli volte contribuisce a far apprendere trucchi sempre nuovi per velocizzare il proprio lavoro.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *