Cas d’usage n°1 : ajouter un item à la liste de courses
Procédons à une petite analyse fonctionnelle :
- entrées : l’élément
- sorties : rien
processus nominal :
- valider l’élément à ajouter ;
- si l’élément est valide, l’ajouter à la liste de course ;
- sinon, ne rien faire.
Tout au long du développement de l’application, on va se référer à une petite liste de tâches (oui, encore une liste) pour nous rappeler ce qu’on doit faire, pour nous garder concentrés, et pour nous dire quand on aura terminé.
Quand on commence à travailler sur un item de la liste, on le mettra en gras, comme ceci. Quand on termine un item, on le rayera, comme cela. Quand on pensera à un item qui n’est pas dans la liste, on l’ajoutera à la liste.
Le premier test de ta première application
À quoi pourrait ressembler, à première vue, cette liste pour notre premier cas d’usage ?
– ajouter un item à la liste de courses
– un item invalide (vide) n’est pas ajouté à la liste de courses
Tu ne vas pas te demander de quel type de donnée ou de quelle procédure tu as besoin.
Tu vas te demander de quel test tu as besoin en premier.
Écrire un test, c’est comme raconter une histoire. Celle de ton opération, vue de l’extérieur.
Même si cette histoire ne s’avérera pas toujours être vraie, je préfère démarrer avec la meilleure API à laquelle je peux penser sur le moment, et faire machine arrière plus tard si ça me bloque.
Ci-dessous, un exemple :
(J’ai volontairement tronqué les lignes de code de déclaration de module, d’import de module, de début et de fin de suite de test. Si c’est gênant pour la compréhension, je suis prêt à les rajouter, n’hésite pas à me faire un retour à ce sujet si besoin).
;; add-grocery-test.scm
(test-assert "add-an-item"
(let* ([test-database #f]
[test-database-insert (lambda (grocery)
(set! test-database (reverse (cons grocery test-database))))]
[add-grocery (make-add-grocery-interactor test-database-insert)]
[given-an-empty-grocery-list (lambda () (set! test-database '()))]
[when-add-grocery (lambda (name) (add-grocery name))]
[then-grocery-is-added (lambda (grocery) (member grocery test-database))])
(begin
(given-an-empty-grocery-list)
(when-add-grocery "tomatoes")
(then-grocery-is-added "tomatoes"))))
Explications :
given-an-empty-grocery-list
,when-add-grocery
etthen-grocery-is-added
sont des petits utilitaires pour un test à la Gherkin.add-grocery
, est un interacteur (traduction littérale, à défaut d’avoir mieux). C’est, en quelque sorte, la procédure centrale de notre cas d’usage.test-database
ettest-database-insert
sont respectivement la base de données de la liste de courses du test et la procédure permettant d’insérer un élément à cette base de données.
Le test ne compile pas, pour l’instant. Le compilateur soulève les erreurs suivantes :
make-add-grocery-interactor
n’est pas définie.
Quel est le minimum que l’on puisse faire pour que ça compile ? Définir, pardi !
Pour faire compiler le test, pas besoin que ces procédures fassent quelque chose. Le but étant de faire compiler le test, pas de le faire passer.
Dans le jargon des tests, on appelle ces implémentations des bouchons.
make-add-grocery-interactor
doit être une procédure qui prend en entrée une procédure et qui retourne une autre procédure, cette dernière prend en entrée un nom et ne semble pas avoir de valeur de retour.
;; add-grocery.scm
(define (make-add-grocery-interactor insert)
(lambda (grocery)
*unspecified*))
Maintenant, on peut lancer le test et le voir échouer ! À ce moment-là, seulement, on peut imaginer le plus petit changement nécessaire pour faire passer le test.
;; add-grocery.scm
(define (make-add-grocery-interactor insert)
(lambda (grocery)
(insert grocery)))
–
ajouter un item à la liste de courses
– un item invalide (vide) n’est pas ajouté à la liste de courses
Au suivant !
–
ajouter un item à la liste de courses
– un item invalide (vide) n’est pas ajouté à la liste de courses
Écrire le test.
;; add-grocery-test.scm
(test-assert "do-not-add-invalid-grocery"
(let* ([test-database #f]
[test-database-insert (lambda (grocery)
(set! test-database (reverse (cons grocery test-database))))]
[add-grocery (make-add-grocery-interactor test-database-insert)]
[given-a-grocery-list (lambda () (set! test-database '("mushrooms" "rice" "potatoes")))]
[when-add-grocery (lambda (name) (add-grocery name))]
[then-grocery-is-not-added (lambda (grocery) (not (member grocery test-database)))])
(begin
(given-a-grocery-list)
(when-add-grocery "")
(then-grocery-is-not-added ""))))
Faire compiler le test et le voir échouer.
Le test compile, mais ne passe pas puisque l’élément ""
a été ajouté à la liste.
Faire passer le test.
;; add-grocery.scm
(define (make-add-grocery-interactor insert)
(lambda (grocery)
(unless (string-null? grocery)
(insert grocery))))
Éliminer les duplications.
Celles-ci se situent dans les tests. Factorisons quelques lignes de code.
D’abord, ce qui concerne la base de données des tests.
;; add-grocery-test.scm
(define test-database #f)
(define (test-database-insert grocery)
(set! test-database (reverse (cons grocery test-database))))
Ensuite, les procédures utilitaires.
;; add-grocery.scm
(define (given-an-empty-grocery-list)
(set! test-database '()))
(define (given-a-grocery-list)
(set! test-database '("mushrooms" "rice" "potatoes")))
(define (when-add-grocery name)
((make-add-grocery-interactor test-database-insert) name))
(define (then-grocery-is-added name)
(member name test-database))
(define (then-grocery-is-not-added name)
(not (then-grocery-is-added name)))
Et enfin les tests.
;; add-grocery.scm
(test-assert "add-a-grocery"
(begin
(given-an-empty-grocery-list)
(when-add-grocery "tomatoes")
(then-grocery-is-added "tomatoes")))
(test-assert "do-not-add-invalid-grocery"
(begin
(given-a-grocery-list)
(when-add-grocery "")
(then-grocery-is-not-added "")))
–
ajouter un item à la liste de courses
–un item invalide (vide) n’est pas ajouté à la liste de courses
Voilà à quoi cela pourrait ressembler
Tu peux copier tout le code suivant dans un fichier add-grocery-test.scm
:
(define-module (add-grocery)
#:export (make-add-grocery-interactor))
(define (make-add-grocery-interactor insert)
(lambda (grocery)
(unless (string-null? grocery)
(insert grocery))))
(define-module (add-grocery-test)
#:use-module (add-grocery)
#:use-module (srfi srfi-64))
(define test-database #f)
(define (test-database-insert grocery)
(set! test-database (reverse (cons grocery test-database))))
(define (given-an-empty-grocery-list)
(set! test-database '()))
(define (given-a-grocery-list)
(set! test-database '("mushrooms" "rice" "potatoes")))
(define (when-add-grocery name)
((make-add-grocery-interactor test-database-insert) name))
(define (then-grocery-is-added name)
(member name test-database))
(define (then-grocery-is-not-added name)
(not (then-grocery-is-added name)))
(test-begin "add-grocery")
(test-assert "add-a-grocery"
(begin
(given-an-empty-grocery-list)
(when-add-grocery "tomatoes")
(then-grocery-is-added "tomatoes")))
(test-assert "do-not-add-invalid-grocery"
(begin
(given-a-grocery-list)
(when-add-grocery "")
(then-grocery-is-not-added "")))
(test-end "add-grocery")
Tu peux ensuite exécuter la commande suivante et observer un résultat similaire :
$ guile --no-auto-compile add-grocery-test.scm
%%%% Starting test add-grocery (Writing full log to "add-grocery.log")
# of expected passes 2