Hello, World
In linea con la tradizione, farò una piccola dimostrazione di ciò che ci si può aspettare nel resto di questo libro, con il famoso esercizio "hello, world".
Ti invito già a creare un piccolo spazio di lavoro (compresa una cartella) in cui memorizzare i codici sorgente di questo libro. Diciamo: ~/Workspace/guile-handbook/
.
Quindi, per l'esercitazione seguente, lavoreremo nella cartella ~/Workspace/guile-handbook/hello/
. Ora crea un file hello.scm
con il seguente contenuto:
(define-module (hello))
(define-public hi
(lambda ()
"hello world\n"))
Come funziona
Guile è un linguaggio molto permissivo. Vale a dire che lascia una grande libertà agli hacker su come scrivere i loro programmi. Quando ho iniziato a scrivere codice Guile, avrei apprezzato un po' più di struttura (o una breve guida...).
Nel esempio, ho scelto di creare un modulo hello
usando la procedura define-module
. In Guile, non devo iniziare necessariamente con un modulo o una funzione main
come in altri linguaggi. Un modulo può essere visto come un insieme di procedure, variabili e macro che si vogliono raggruppare.
La procedura define-public
espone il simbolo hi
, che è collegato a una procedura che non prende argomenti in ingresso e restituisce la stringa hello world\n
.
Come fare il test
Sempre nella cartella ~/Workspace/guile-handbook/hello/
, crea un file hello-test.scm
:
(use-modules (srfi srfi-64)
(hello))
(test-begin "harness")
(test-equal "test-hello"
"hello world\n"
(hi))
(test-end "harness")
Per eseguire il test, digita il seguente comando:
$ guile -L . hello-test.scm
L'opzione -L
indica che per questo comando, voglio aggiungere la cartella corrente (.
) all'inizio del percorso dei moduli Guile da caricare. Altrimenti, Guile non saprebbe che il modulo hello
esiste.
Preferisco questo modo, che non altera in modo permanente il percorso dei materiali didattici
Scrivere i test
Non è necessario cercare, scegliere o installare un framework per i test. Guile contiene già il modulo srfi-64
(un framework per i test per il linguaggio Scheme).
Non è necessario aggiungere un prefisso o un suffisso al nome del file o della funzione.
La procedura use-modules
ci permette di importare i moduli che ci servono per i nostri test:
- il modulo
srfi-64
- il modulo
hello
, quello che vogliamo testare.
Ai fini del funzionamento del framework di test, le procedure test-begin
e test-end
indicano rispettivamente dove il test inizia e dove finisce. È necessario assegnargli un nome fornendo una stringa, in questo esempio: "harness"
.
Un test è una chiamata a una delle procedure fornite dal modulo srfi-64
. In questo caso, si tratta della procedura test-equal
che prende 3 argomenti:
- il nome del testo:
"test-hello"
- Il valore atteso:
"hello world\n"
- l'espressione che vogliamo testare:
(hi)
.
Se il valore atteso è uguale al valore risultante dalla valutazione dell'espressione testata, il test viene superato. In caso contrario, il test fallisce.
Quando si eseguono i test, i risultati forniti sono molto sintetici. Per impostazione predefinita, il framework fornisce maggiori dettagli nel file di log name-of-the-test-suite-te-tests.log
.
Hello, You
Nell'esempio precedente, ho scritto il codice da testare prima di scrivere il test. Nel resto di questo libro, seguirò i passi del metodo Test Driven Development: red - green - refactor.
Con un test che può essere eseguito rapidamente, posso iterare il programma in modo sicuro. Se una funzione si blocca, me ne accorgo immediatamente! Si chiama ciclo di feedback.
Ora torniamo al codice precedente e aggiungiamo un requisito: Voglio essere in grado di specificare a chi mando i miei saluti.
Red
Inizio aggiungendo, nella mia suite di prova, il codice che riflette questo requisito:
(use-modules (srfi srfi-64)
(hello))
(test-begin "harness")
(test-equal "test-hello"
"hello world\n"
(hi))
(test-equal "test-named-hello"
"hello Jérémy\n"
(hi "Jérémy"))
(test-end "harness")
Se si esegue la suite di prova, si può osservare che il nostro primo test passa, ma il nuovo test fallisce. Questo passo ci assicura di non scrivere un test che sia un falso positivo perché non testa il comportamento giusto.
;;; note: source file /home/jeko/Workspace/guile-handbook/hello/hello-test.scm
;;; newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello-test.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook/hello/hello-test.scm
;;; hello-test.scm:12:2: warning: possibly wrong number of arguments to `hi'
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello-test.scm.go
%%%% Starting test harness (Writing full log to "harness.log")
hello-test.scm:10: FAIL test-named-hello
# of expected passes 1
# of unexpected failures 1
Inoltre, il compilatore è di grande aiuto per spiegare perché un codice non funziona. In questo caso, mi avverte che nella riga 12 del mio file di prova, utilizzo la procedura hi
con un numero sbagliato di argomenti.
Green
Adesso cambierò il mio codice per fare in modo che il test passi.
Per prima cosa, inizierò a correggere i "warning" segnalati in fase di compilazione. Per fare questo, faccio in modo che la procedura hi
possa gestire un parametro opzionale:
(define-module (hello))
(define-public hi
(lambda* (#:optional name)
"hello world\n"))
Ora che il compilatore non si lamenta più, cerco nel file di log della mia suite di test, cat harness.log
, il motivo del fallimento:
%%%% Starting test harness
Group begin: harness
Test begin:
test-name: "test-hello"
source-file: "hello-test.scm"
source-line: 6
source-form: (test-equal "test-hello" "hello world\n" (hi))
Test end:
result-kind: pass
actual-value: "hello world\n"
expected-value: "hello world\n"
Test begin:
test-name: "test-named-hello"
source-file: "hello-test.scm"
source-line: 10
source-form: (test-equal "test-named-hello" "hello Jérémy\n" (hi "Jérémy"))
Test end:
result-kind: fail
actual-value: "hello world\n"
expected-value: "hello Jérémy\n"
Group end: harness
# of expected passes 1
# of unexpected failures 1
Il test "test-named-hello"
fallisce perché si aspetta il valore "hello Jeremy"
ma trova "hello world"
. Lo correggo:
(define-module (hello))
(define-public hi
(lambda* (#:optional name)
(if name
"hello Jérémy\n"
"hello world\n")))
Questa volta, l'esecuzione della suite di test mostra che il nuovo codice ha corretto il secondo test senza bloccare il primo.
;;; note: source file ./hello.scm
;;; newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling ./hello.scm
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/hello/hello.scm.go
%%%% Starting test harness (Writing full log to "harness.log")
# of expected passes 2
La mia suite di test può essere vista come un elenco di casi d'uso del mio software. È più pragmatico di una documentazione. Senza alcuna conoscenza del codice operativo, posso vedere rapidamente che il software può essere chiamato senza o con un parametro e che cosa restituisce.
Controllo di versione
Allo stato attuale, il codice di produzione è operativo, come mostrato dai test green. Si raccomanda di fare un "commit" di questo stato del codice prima del passo successivo (questo permetterà di tornare alla versione funzionante).
Tuttavia, poiché il ciclo red-green-refactor non è stato completato, non c'è motivo di fare il commit. Il codice è funzionale, ma non completo.
Refactor
Riformulare il codice significa eliminare i duplicati e rendere il codice più esplicito. Per me, questi sono i principi più importanti, da cui derivano tutti gli altri.
E precisamente, nel codice, possiamo notare dei duplicati, sia nei valori che nella costruzione delle seguenti stringhe:
"hello Jérémy \n"
"hello world \n"
Li eliminerò con l'estrazione di una procedura che non sarà esposta pubblicamente e con delle costanti che daranno più significato ai valori.
(define-module (hello))
(define GREETING_PREFIX "hello ")
(define GREETING_SUFFIX "\n")
(define DEFAULT_ADDRESSEE "world")
(define-public hi
(lambda* (#:optional name)
(string-append GREETING_PREFIX (addressee name) GREETING_SUFFIX)))
(define addressee
(lambda (name)
(if name
"Jérémy"
DEFAULT_ADDRESSEE)))
Torniamo al controllo di versione
A questo punto posso modificare
volentieri il commit precedente per mantenere solo questa versione impeccabile del nostro codice e dei test.
Hello, You… ancora
Nell'esempio precedente, ho deliberatamente spinto il concetto di "piccoli passi" molto lontano per ragioni didattiche. Ma questo mi permette di introdurre una tecnica TDD: Triangulation.
Red
(use-modules (srfi srfi-64)
(hello))
(test-begin "harness")
(test-equal "test-hello"
"hello world\n"
(hi))
(test-equal "test-named-hello"
"hello Jérémy\n"
(hi "Jérémy"))
(test-equal "test-named-hello-bis"
"hello Hacker\n"
(hi "Hacker"))
(test-end "harness")
Vedo che il test fallito nella suite di test.
Green
(define-module (hello))
(define GREETING_PREFIX "hello ")
(define GREETING_SUFFIX "\n")
(define DEFAULT_ADDRESSEE "world")
(define-public hi
(lambda* (#:optional name)
(string-append GREETING_PREFIX (addressee name) GREETING_SUFFIX)))
(define addressee
(lambda (name)
(if name
name
DEFAULT_ADDRESSEE)))
Il cambiamento è minimo ma evidente. Questo è uno dei vantaggi di costringersi a lavorare a "piccoli passi".
Ora tutti i test passano.
Refactor
Nella fase di refactoring, si deve rielaborare tutto il codice. Questo include i test!
I test sono una chiara specifica di ciò che il codice deve fare. Quanto più esplicita è la suite di test, tanto più l'utente capirà come funziona il programma.
(use-modules (srfi srfi-64)
(hello))
(test-begin "harness")
(define (assert-correct-message test-name test-expected test-effective)
(test-equal test-name test-expected test-effective))
(assert-correct-message "test-default-hello" "hello world\n" (hi))
(test-equal "test-named-hello"
"hello Jérémy\n"
(hi "Jérémy"))
(test-equal "test-named-hello-bis"
"hello Hacker\n"
(hi "Hacker"))
(test-end "harness")
Conclusione
- Introduzione TDD
- Disciplina
- Rispetto del ciclo Red-Green-Refactor
- Parametri opzionali per le procedure