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 :

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 :

  1. faire un paquet
  2. utiliser Quicklisp

Faire un paquet

Pour faire un paquet, j’utilise deux fichiers à la racine du projet :

  • jazz.lisp
  • jazz.asd

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)