Tests unitaires en Lisp
Contenu
Lancer des tests unitaires, ce devrait être simple, ce devrait être automatique. Et bien cela ne m’a pas paru très évident à faire en Common Lisp. Voici donc ma petite excursion aux pays des expressions symboliques.
Louanges !
Au premier jour il fut un langage vieux, très vieux, plus vieux que moi. Certainement plus vieux que toi, que lui ou elle. Puis fut venu le temps de la consécration, dans les années 80. Une norme ANSI qui a mis du temps à accoucher. Le déclin. Les multiples implémentations. Les
- T’es plutôt Scheme ou Common Lisp ?
- Moi je fais du CLOS
- Ennemi du Vim, j’édite du Lisp plus vite que John McCarthy
- Bonjour je me nomme Paul G. et j’ai fait Arc
Mais c’était sans compter d’irresistibles guerriers, chevaliers de la parenthèse qui ont su faire briller encore et toujours cette incroyable syntaxe, ce merveilleux langage dérivé de Lisp, qui lie simplicité et puissance.
Et désespoir…
Trêve de grandiloquence ! Y’a des fois, où c’est mal documenté. Tu galères pour faire un truc simple. Un truc qui te prend pas grand chose, voire rien à faire, dans d’autres langages/outils. Par exemple pour faire des tests unitaires.
En Python, il y a unittest
. C’est dedans, c’est compris, c’est permis. C’est
pas la plus sexy ni la moins verbeuse des façons de faire des tests en
Python. Je lui préfère pytest, mais tu
écris un ou plusieurs fichiers avec des trucs comme :
def test_should_be_right():
assert foo(arg) == expected
tu lances pytest
and voilà !
En Clojure, on utilise souvent un outil qui télécharge les dépendances,
construit le projet, démarre une REPL, lance les tests. Le plus connu, c’est
leiningen. Un lein test
, et puis c’est tout !!
En Common Lisp, on se pose déjà la question de quelle implémentation on va utiliser : sbcl, CLisp, AllegroCL, etc. Des bibliothèques de tests unitaires, il y en a quelques unes, lisp-unit, prove, et bien plus encore. Mais le choix de cette dernière ne tombe pas à l’évidence : projet âgé, source non maintenue, peu documentée. Et P.Seibel, dans le très très bon Practical Common Lisp (complètement disponible en ligne), va même jusqu’à écrire un chapitre sur le sujet : construire son propre framework de test.
Quelques essais
Partons sur lisp-unit. Le wiki dédié est plutôt bien. On peut l’installer et le charger avec Quicklisp. Puis un exemple assez simple :
(define-test test-blue-train
(assert-equal "Blue Train" (blue-train 1))
(assert-equal "Lazy Bird" (blue-train 5)))
Puis (run-tests '(test-blue-train))
ou (run-tests :all)
. Mais les exemples
décrits sont souvent à faire dans une REPL. Pas grand chose à se mettre sous la dent
quand on veut exécuter le fichier .lisp
pour lancer tous les tests, au mieux avec
des filtres, et avoir un minimum d’info sur la sortie standard.
En pratique
Première question qui m’a laissé sur le carreau quelques temps : “comment je lance un programme Common Lisp en tant qu’exécutable ?“. Il y a :
- http://www.xach.com/lisp/buildapp/
- https://github.com/roswell/roswell et qui fait aussi gestionnaire de paquets à l’instar de Quicklisp
Je pense que ces outils sont surtout pour construire des exécutables compilés. Pas pour charger un fichier Lisp et lancer des tests.
Du coup, j’essaie d’abord :
#!/usr/bin/sbcl --script
;; -*- mode: lisp -*-
(load "my-test.lisp")
dans un simple launch.lisp
que je rend exécutable. Mon my-test.lisp
a bien
sûr une dépendance sur lisp-unit et aussi sur mon propre paquet. Par exemple au
début du fichier, on peut avoir un truc comme ça :
(in-package :cl-user)
(defpackage my-test
(:use :cl
:lisp-unit
:jazz))
(in-package :my-test)
Et à l’exécution de mon script, j’ai un : The name "LISP-UNIT" does not designate
any package.
alors que je sais que je l’ai installé. Deux trucs à faire :
- faire un paquet
- utiliser Quicklisp
Faire un paquet
Pour faire un paquet, j’utilise deux fichiers à la racine du projet :
jazz.lisp
jazz.asd
où jazz
est le nom de mon paquet.
Le fichier jazz.lisp
qui définit le paquet : les dépendances, les fonctions à
exporter. Je peux mettre le code source de mon paquet dans ce fichier, mais je ne
suis pas obligé, i.e. on sépare tout ça dans plusieurs fichiers Lisp.
;;;; jazz.lisp
(defpackage #:jazz
(:use #:cl)
(:export :blue-train))
(in-package :jazz)
(defun blue-train (num)
"return the title according to its number"
(case num
(1 "Blue Train")
(2 "Moment's Notice")
(3 "Locomotion")
(4 "I'm Old Fashioned")
(5 "Lazy Bird")
(otherwise "not found")))
Le fichier jazz.asd
qui définit le system pour construire/compiler et charger les
sources du paquet. Indispensable quand je veux distribuer mon code.
;;;; jazz.asd
(asdf:defsystem #:jazz
:description "So Jazz!"
:author "Damien Garaud"
:license "WTFPL"
:depends-on (#:lisp-unit)
:serial t
:components ((:file "jazz")))
Pour plus de précisions, lire aussi :
Utiliser Quicklisp
Après avoir installé Quicklisp, c’est toujours mieux de le rendre disponible à l’initialisation de votre meilleure implémentation de Lisp. Cette opération est très bien indiquée à la fin de l’installation par ailleurs.
To load Quicklisp every time you start Lisp, use: (ql:add-to-init-file)
Côté implémentation, je conseille très fortement d’utiliser sbcl.
Ensuite, il “suffit” de modifier un peu le fichier launch.lisp
.
#!/usr/bin/sbcl --script
;; -*- mode: lisp -*-
#-quicklisp
(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
(user-homedir-pathname))))
(when (probe-file quicklisp-init)
(load quicklisp-init)))
(ql:quickload 'jazz)
(load "my-test.lisp")
Enfin, pour que la ligne (ql:quickload 'jazz)
ne vous sorte pas un System "jazz"
not found
, il faut ajouter le paquet aux projets locaux de
Quicklisp. C’est souvent ici :
~/quicklisp/local-projects
. Et je fais un lien symbolique
~/quicklisp/local-projects/jazz
vers le code source.
Et pous avoir le plus d’informations possibles lors de l’exécution des tests, c’est pas mal de rajouter :
;; To see the failures
(setq *print-failures* t)
(setq *print-errors* t)
avant les test cases. On lance ./launch.lisp
et on a quelque chose comme
ça :
To load "jazz":
Load 1 ASDF system:
jazz
; Loading "jazz"
[package jazz]
TEST-BLUE-TRAIN: 2 assertions passed, 0 failed.
Unit Test Summary
| 2 assertions total
| 2 passed
| 0 failed
| 0 execution errors
| 0 missing tests
Conclusions
Avec toutes ces briques et cette configuration de paquet, on est de suite plus à l’aise pour tester le code qu’on écrit.
Ce n’est parfois pas évident de trouver des projets Open Source avec ces bonnes pratiques, ces tests unitaires et une façon simple de les lancer, surtout en Common Lisp qui est loin d’être mainstream. Projets qu’on aimerait trouver pour ainsi s’en inspirer, quitte à faire du copier-coller-modifier qui fonctionne “même si j’ai pas tout compris”.
Vous pouvez retrouver un exemple similaire à cet article dans “mon bac à sable” Common Lisp sur Github https://github.com/garaud/cl-sandbox/tree/master/jazz et aussi pas mal de tests unitaires qui reproduisent les solutions de 99 Lisp Problems dans le même projet (mes 99 problems avec tests unitaires)