Caso d'uso 1: Aggiungere un articolo alla lista della spesa
Il caso d'uso è relativamente semplice. Vogliamo aggiungere l'elemento passato in input al programma alla fine di un elenco. Non ci aspettiamo nulla in cambio.
Durante lo sviluppo dell'applicazione, faremo riferimento a un piccolo elenco di compiti (sì, un altro elenco) per ricordarci cosa dobbiamo fare, per tenerci concentrati e per capire quando abbiamo finito.
Quando iniziamo a lavorare su un elemento dell'elenco, lo mettiamo in grassetto, come questo. Quando terminiamo un elemento, lo cancelliamo, come questo. Quando pensiamo a un nuovo test da scrivere, per esempio, lo aggiungiamo all'elenco.
Il primo test della prima applicazione
Come sarebbe questo elenco per il nostro primo caso d'uso?
aggiungere un elemento a un nuovo elenco
aggiungere un elemento a un elenco esistente
non aggiungere un elemento vuoto all'elenco
Come puoi vedere, dovrai lavorare sull'aggiunta di un elemento a un nuovo elenco. Non ti chiederai quale tipo di dati o quale procedura ti serva. Ti chiederai invece di quale test hai bisogno per prima cosa.
Scrivere un test è come raccontare una storia. La storia della tua operazione dall'esterno. Anche se questa storia non si rivelerà sempre vera, preferisco iniziare con la migliore API che mi viene in mente in quel momento e fare un passo indietro se mi blocca.
Ecco un semplice esempio di aggiunta di un elemento a un nuovo elenco (si possono copiare le seguenti righe nel file grocery-list-test.scm
):
(define-module (grocery-list-test)
#:use-module (srfi srfi-64))
(test-begin "grocery-list")
(grocery-list-add "tomatoes")
(test-equal '("tomatoes") (grocery-list-get))
(test-end "grocery-list")
Questo non è certo il codice Guile più idiomatico: ha un nessun effetto collaterale soprattutto su una variabile nascosta. Prenderemo nota di queste cose che ci danno fastidio e continueremo il nostro ciclo di sviluppo guidato dai test.
aggiungere un elemento a una lista nuova
aggiungere un elemento a una lista esistente
non aggiungere un elemento vuoto alla lista
effetti collaterali?
viariabili nascoste?
Il test che hai appena scritto non viene nemmeno compilato. È abbastanza facile da correggere.
Si possono notare due errori di compilazione:
grocery-list-add
non è definitagrocery-list
neanche lei.
Quali sono le modifiche minime necessarie alla compilazione?
Per compilare il test, non è necessario che queste procedure facciano qualcosa. Di seguito è riportata una definizione minima di procedura (ho scelto di farle restituire '()
, che credo sia una sorta di valore nullo). Nel gergo dei test, queste implementazioni sono chiamate stubs.
Procediamo con un errore alla volta.
(define (grocery-list-add item)
'())
Un errore in meno, uno da sistemare.
(define (grocery-list-get)
'())
Ora possiamo eseguire il test.
$ guile --no-auto-compile grocery-list-test.scm
%%%% Starting test grocery-list (Writing full log to "grocery-list.log")
/home/jeko/Workspace/guile-grocery-list/grocery-list-test.scm:13: FAIL
# of unexpected failures 1
Il test fallisce fallisce.
Nello sviluppo guidato dai test, il fallimento è un progresso! Questo ci dà una misura concreta. Invece di affrontare il problema in modo olistico ("aggiungere un elemento a un elenco"), lo si affronta in modo sequenziale ("eseguire questo test, poi il prossimo ecc..."). Si restringe l'ambito del test finché non si è sicuri che funzioni.
Il report del test grocery-list.log
fornisce la ragione del fallimento.
È fastidioso dover aprire il rapporto a mano, non c'è ragione di continuare in questo modo; il framework di test srfi-64
permette di cambiare questo comportamento, ma non ho intenzione di parlarne qui.
Quindi, leggiamo il report:
cat grocery-list.log
%%%% Starting test grocery-list
Group begin: grocery-list
Test begin:
source-file: "/home/jeko/Workspace/guile-grocery-list/grocery-list-test.scm"
source-line: 13
source-form: (test-equal (quote ("tomatoes")) (grocery-list-get))
Test end:
result-kind: fail
actual-value: ()
expected-value: ("tomatoes")
Group end: grocery-list
# of unexpected failures 1
Ci aspettiamo il valore ("tomatoes")
, ma abbiamo ottenuto ()
. Forse non vi piacerà ciò che segue, ma l'obiettivo non è fornire la soluzione perfetta per superare il test, ma la più piccola modifica che si possa immaginare per superare il test.
(define (grocery-list-get)
'("tomatoes"))
Non ha molto senso. Ma è piccola...
$ guile --no-auto-compile grocery-list-test.scm
%%%% Starting test grocery-list (Writing full log to "grocery-list.log")
# of expected passes 1
...e supera il test. Vittoria! Si può passare all'ultima fase del ciclo, il refactoring!
Reminder
Un ciclo di sviluppo guidato dai test è composto da questi fasi:
- Aggiungere un piccolo test.
- Eseguire il test che fallisce.
- Fare le opportune modifiche
- Eseguire nuovamente il test (adesso passa)
- Rimuovere i duplicati
L'ultimo passo serve a eliminare i doppioni. Nel codice che avete scritto finora, vedete qualche duplicato?
Tra il codice e il test ce n'è uno con il valore "tomatoes"
.
Questo dato proviene dal test, è il valore del parametro passato alla procedura grocery-list-add
. Possiamo salvarlo in una variabile.
(define grocery-list '("tomatoes"))
Quindi fare (grocery-list-get)
per restituire il contenuto di questa variabile:
(define (grocery-list-get)
grocery-list)
È possibile eseguire nuovamente il test ogni volta che si apporta una modifica e per verificare che passi!
Poi, ho detto che "tomatoes"
proviene dal parametro passato alla procedura grocery-list-add
. È possibile modificare questa procedura in modo che valorizzi grocery-list
con il valore giusto.
(define (grocery-list-add item)
(set! grocery-list (list "tomatoes")))
grocery-list
può ora essere inizializzata con un elenco vuoto.
(define grocery-list '())
Per la modifica finale, grocery-list-add
utilizzerà il valore del suo parametro!
(define (grocery-list-add item)
(set! grocery-list (list item)))
I passi verso la soluzione devono essere così piccoli? No, ma quando le cose si fanno davvero strane, è sempre utile sapere che si può fare. Inoltre, chi può fare di meno può fare di più.
aggiungere un elemento a un nuovo elenco
aggiungere un elemento a un elenco esistente
non aggiungere un elemento vuoto all'elenco
effetti collaterali?
variabile nascosta?
Ora possiamo contrassegnare il primo test della nostra lista come completo e fare una piccola revisione di ciò che avete fatto finora:
- abbiamo fatto un elenco di test che sappiamo di dover far funzionare
- raccontato una storia con il codice di come volevamo che funzionasse l'operazione
- ignorato i dettagli del framework di test e del suo rapporto di test
- compilato il test grazie agli stub
- abbiamo fatto in modo che il test passasse come se fossimo il programmatore più pigro di tutti i tempi
- abbiamo generalizzato il codice che funziona sostituendo le costanti con le variabili
- abbiamo aggiunto elementi alla nostra lista di compiti piuttosto che affrontarli tutti insieme
Principio della responsabilità unica
Finora, la nostra modesta base di codice si presenta così: un file grocery-list-test.scm
(oltre al file main.scm
del capitolo precedente).
tree guile-grocery-list/
guile-grocery-list
├── grocery-list.log
├── grocery-list-test.scm
└── main.scm
1 directory, 3 files
Come suggerisce il nome del file, contiene dei test. Ma questo non è vero, intendo parzialmente. Questo file contiene un test e del codice!
(define-module (grocery-list-test)
#:use-module (srfi srfi-64))
(define grocery-list '())
(define (grocery-list-add item)
(set! grocery-list (list item))
(define (grocery-list-get)
grocery-list)
(test-begin "grocery-list")
(grocery-list-add "tomatoes")
(test-equal '("tomatoes") (grocery-list-get))
(test-end "grocery-list")
Ha due responsabilità, due ragioni per cambiare. Dannazione!
Tra i principi SOLID, il "Single Responsibility Principle" (SRP) significa che un modulo deve essere responsabile nei confronti di un solo attore.
Si legge la parola "attore" che può indicare una procedura, un modulo, un file sorgente, un componente...
Poiché siamo ancora in uno stato in cui il test passa, ne approfitteremo per perfezionare il tutto. Infine, il refactoring non è finito!
Si separeranno il test e il codice in file e moduli diversi.
Crea un file grocery-list.scm
e definisci il modulo nel codiee:
(define-module (grocery-list))
Importa il modulo appena creato nel modulo di test:
(define-module (grocery-list-test)
#:use-module (srfi srfi-64)
#:use-module (grocery-list))
Ora, per eseguire il test dalla riga di comando, devi aggiungere la cartella del progetto al load-path di Guile (altrimenti non sarà in grado di trovare la definizione del modulo grocery-list):
$ guile --no-auto-compile -L . grocery-list-test.scm
%%%% Starting test grocery-list (Writing full log to "grocery-list.log")
# of expected passes 1
Ora è possibile spostare il codice, un po' alla volta.
Inizia con la variabile grocery-list
. Aggiungi la sua definizione al modulo grocery-list
, facendo attenzione alla sua direttiva export (spero di non confonderti con nomi ridondanti)
(define-module (grocery-list)
#:export (grocery-list))
(define grocery-list '())
Se il test passa ancora, si può rimuovere la sua definizione dal modulo di test.
Quindi, ripeti questi passaggi per grocery-list-add
.
E infine per grocery-list-get
.
Se le mie istruzioni sono state abbastanza chiare, ora si ha un file grocery-list-test.scm
contenente:
(define-module (grocery-list-test)
#:use-module (srfi srfi-64)
#:use-module (grocery-list))
(test-begin "grocery-list")
(grocery-list-add "tomatoes")
(test-equal '("tomatoes") (grocery-list-get))
(test-end "grocery-list")
e un file grocery-list.scm
contenente:
(define-module (grocery-list)
#:export (grocery-list
grocery-list-add
grocery-list-get))
(define grocery-list '())
(define (grocery-list-add item)
(set! grocery-list (list item))
(define (grocery-list-get)
grocery-list)
Rivediamo questo lavoro:
- allungare la fase di rilavorazione, sempre con i test verdi
- affrontare un principio SOLID, SRP
- separare i test dal codice in file e moduli diversi