Cas d’usage n°1
Tout au long du développement de l’application, on va se référer à une petite liste de tâches 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 marquera avec un x (je préfère le rayer, mais je n'ai pas de quoi le faire ici pour l'instant - ceci est un appel à étendre guile-commonmark , première feature bounty officielle pour le GHH ?). Quand on pensera à un item qui n’est pas dans la liste, on l’ajoutera à la liste.
Créer un bounty
Tout d'abord, une petite analyse fonctionnelle. De quoi est-ce que j'ai besoin pour créer un bounty digne de ce nom ? Et qu'est-ce que j'attend en retour lorsque la création réussie ou échoue ?
- entrées : l'adresse e-mail du sponsor, un titre, une description, une récompense, des critères d'acceptation et une date limite (optionnelle).
- sorties : le statut de la création et son détail (si succès : confirmation de l'opération ; si échec : la raison de l'erreur)
scénario nominal :
l'utilisateur remplit le formulaire de création de bounty
le système enregistre le bounty
le système retourne le résultat de la création
Le premier test de ta première application
À quoi pourrait ressembler, à première vue, cette liste pour notre premier cas d’usage ? J'ai choisi d'aller droit au but car ce qui va suivre est déjà bien dense. Mais tu peux t'amuser à décomposer encore plus le développement.
– créer un bounty - nominal
– créer un bounty - échec
Tu ne vas pas te demander de quel structure 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.
Puisque je vais guider le développement de l'application par les tests, je les développerai en suivant les mêmes concepts de Clean Architecture et de Clean Code que le reste du code.
- le controller est responsable de transmettre notre requête à l'interactor en lui fournissant une requete.
- le presenter est responsable de traiter la réponse de l'interactor et le résultat de ce traitement servira à mettre à jour le view-model
- la view ce sont les assertions des tests sur le view-model
- l'interactor devra faire persister les données via un repository
Cela étant dit, voici à quoi pourrait ressembler l'histoire de mon premier test à la mode Dijkstra :
;; tests/feature-bounties-test.scm
(use-modules (srfi srfi-64))
(test-begin "create-bounties")
(test-group "create-one-bounty"
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-is-successul)
(then-bounty-is-in-persistence dummy-bounty))
(test-end "create-bounties")Le test ne compile pas car l'interpréteur me dit que make-feature-bounty n'est pas définie.
;; src/entities.scm
(define-module (src entities)
#:use-module (srfi srfi-9)
#:export (make-feature-bounty
feature-bounty-title
feature-bounty-email
feature-bounty-description
feature-bounty-reward
feature-bounty-due-date))
(define-record-type <feature-bounty>
(make-feature-bounty email title description reward due-date)
feature-bounty?
(email feature-bounty-email)
(title feature-bounty-title)
(description feature-bounty-description)
(reward feature-bounty-reward)
(due-date feature-bounty-due-date))feature-bounty est ce qu'on appelle une entity dans le jargon de la Clean Architecture.
Relance les tests.
Maintenant, l'interpréteur me dit que c'est when-create-feature-bounty qui n'est pas défini. C'est la procédure qui va appeler notre interactor et passer le résultat de l'opération au presenter. Autrement dit, when-create-feature-bounty est le controller de ce cas d'usage pour nos tests. Ça fait pas mal de choses à ajouter donc on va prendre notre temps.
On va commencer par définir notre interactor. Ce dernier doit traiter une requete, persister des données (c'est le rôle du repository) et retourner une réponse contenant le résultat de l'opération (confirmation de succès ou d'échec). Cet interactor est accompagné de ses structures de données d'entrée et de sortie.
;; src/use-cases/create-feature-bounty.scm
(define-module (src use-cases create-feature-bounty)
#:use-module (src entities)
#:use-module (srfi srfi-9)
#:export (make-feature-bounty-repository
make-create-feature-bounty-interactor
make-create-feature-bounty-request
make-create-feature-bounty-response
create-feature-bounty-response-status))
(define-record-type <create-feature-bounty-request>
(make-create-feature-bounty-request email title description reward due-date)
create-feature-bounty-request?
(email create-feature-bounty-request-email)
(title create-feature-bounty-request-title)
(description create-feature-bounty-request-description)
(reward create-feature-bounty-request-reward)
(due-date create-feature-bounty-request-due-date))
(define-record-type <create-feature-bounty-response>
(make-create-feature-bounty-response status)
create-feature-bounty-response?
(status create-feature-bounty-response-status))
(define-record-type <feature-bounty-repository>
(make-feature-bounty-repository create)
feature-bounty-repository?
(create feature-bounty-repository-create))
(define (make-create-feature-bounty-interactor bounty-repository)
(lambda (request)
(let ([bounty (make-feature-bounty
(create-feature-bounty-request-title request)
(create-feature-bounty-request-email request)
(create-feature-bounty-request-description request)
(create-feature-bounty-request-reward request)
(create-feature-bounty-request-due-date request))])
(begin
((feature-bounty-repository-create bounty-repository) bounty)
(make-create-feature-bounty-response 'success)))))Je ne sais pas encore quelle sera la technologie de persistence. Alors pour le besoin de nos tests, on fera avec la mémoire vive tout en gardant la possibilité de changer ça plus tard (c'est l'idée derrière le concept de repository).
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src use-cases create-feature-bounty)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-is-successul)
(then-bounty-is-in-persistence dummy-bounty)
(test-end "create-bounties")Vient ensuite le presenter et le view-model.
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define create-feature-bounty-view-model-for-test '())
(define (create-feature-bounty-presenter-for-test response)
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"success?"
(if
(eq? 'success (create-feature-bounty-response-status response))
#t
#f))))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-is-successul)
(then-bounty-is-in-persistence dummy-bounty)
(test-end "create-bounties")Nous avons tout ce qu'il faut pour définir le controller.
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define create-feature-bounty-view-model-for-test '())
(define (create-feature-bounty-presenter-for-test response)
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"success?"
(if
(eq? 'success (create-feature-bounty-response-status response))
#t
#f))))
(define* (when-create-feature-bounty email title description reward
#:optional due-date)
(let ([interactor-for-test
(make-create-feature-bounty-interactor feature-bounty-repository-for-test)])
(create-feature-bounty-presenter-for-test
(interactor-for-test
(make-create-feature-bounty-request
(make-feature-bounty email title description reward due-date))))))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-is-successul)
(then-bounty-is-in-persistence dummy-bounty)
(test-end "create-bounties")Et enfin, les then… !
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define create-feature-bounty-view-model-for-test '())
(define (create-feature-bounty-presenter-for-test response)
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"success?"
(if
(eq? 'success (create-feature-bounty-response-status response))
#t
#f))))
(define* (when-create-feature-bounty email title description reward
#:optional due-date)
(let ([interactor-for-test
(make-create-feature-bounty-interactor feature-bounty-repository-for-test)])
(create-feature-bounty-presenter-for-test
(interactor-for-test
(make-create-feature-bounty-request
(make-feature-bounty email title description reward due-date))))))
(define (then-creation-is-successul)
(test-assert (assoc-ref create-feature-bounty-view-model-for-test "success?")))
(define (then-bounty-is-in-persistence bounty)
(test-assert (member bounty bounties)))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-is-successul)
(then-bounty-is-in-persistence dummy-bounty)
(test-end "create-bounties")À présent, le test compile et passe ! Pfiouu… Notre application capable de créer un feature bounty !
Histoire de dire que j'ai pris le temps de réusiner le code, je vais alléger un peu le fichier de test. Je vais extraire les définitions des controller, presenter et view-model :
;; tests/interface-adapters.scm
(define-module (tests interface-adapters)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:export (create-feature-bounty-presenter-for-test
create-feature-bounty-view-model-for-test
create-feature-bounty-controller-for-test))
(define create-feature-bounty-view-model-for-test '())
(define (create-feature-bounty-presenter-for-test response)
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"success?"
(if
(eq? 'success (create-feature-bounty-response-status response))
#t
#f)))
(define (create-feature-bounty-controller-for-test interactor)
(lambda* (email title description reward #:optional due-date)
(create-feature-bounty-presenter-for-test
(interactor
(make-create-feature-bounty-request email title description reward due-date)))))Le fichier des tests est ainsi plus focalisé sur les tests en eux-même :
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:use-module (tests interface-adapters)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define when-create-feature-bounty
(create-feature-bounty-controller-for-test
(make-create-feature-bounty-interactor feature-bounty-repository-for-test)))
(define (then-creation-success-is-reported)
(test-assert (assoc-ref create-feature-bounty-view-model-for-test "success?")))
(define (then-bounty-is-in-persistence bounty)
(test-assert (member bounty bounties)))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-success-is-reported)
(then-bounty-is-in-persistence dummy-bounty)
(test-end "create-bounties")Voilà ce que ça donne dans le système de fichier :
├── src
│ ├── entities.scm
│ └── use-cases
│ └── create-feature-bounty.scm
└── tests
├── feature-bounties-test.scm
└── interface-adapters.scmLe deuxième test
Maintenant que notre premier test est OK, on va passer au deuxième pour boucler ce premier cas d'usage.
– x créer un bounty - nominal
– créer un bounty - échec
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:use-module (tests interface-adapters)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define when-create-feature-bounty
(create-feature-bounty-controller-for-test
(make-create-feature-bounty-interactor feature-bounty-repository-for-test)))
(define (then-creation-success-is-reported)
(test-assert (assoc-ref create-feature-bounty-view-model-for-test "success?")))
(define (then-bounty-is-in-persistence bounty)
(test-assert (member bounty bounties)))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-success-is-reported)
(then-bounty-is-in-persistence dummy-bounty)
(clear-persistence)
(given-a-broken-repository)
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-failure-is-reported)
(then-failure-reason-is-provided)
(then-bounty-is-not-in-persistence)
(test-end "create-bounties")On exécute le test, pour lire de l'interpréteur que clear-persistence est à définir, puis given-a-broken-repository, then-creation-failure-is-reported, then-failure-reason-is-provided et enfin then-bounty-is-not-in-persistence. À nous de nous exécuter.
Il faudra alors ajouter un champ reason à la réponse de l'interactor.
;; tests/feature-bounties-test.scm
(define-module (tests feature-bounties)
#:use-module (srfi srfi-64)
#:use-module (ice-9 exceptions)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:use-module (tests interface-adapters)
#:export ())
(define bounties '())
(define feature-bounty-repository-for-test
(make-feature-bounty-repository
(lambda (bounty)
(set! bounties (append bounties (list bounty))))))
(define when-create-feature-bounty
(create-feature-bounty-controller-for-test
(make-create-feature-bounty-interactor feature-bounty-repository-for-test)))
(define (given-a-broken-repository)
(set! when-create-feature-bounty
(create-feature-bounty-controller-for-test
(make-create-feature-bounty-interactor
(make-feature-bounty-repository
(lambda (bounty)
(raise-exception
(make-exception-with-message "Persistence is broken"))))))))
(define (then-creation-success-is-reported)
(test-assert (assoc-ref create-feature-bounty-view-model-for-test "success?")))
(define (then-creation-failure-is-reported)
(test-assert (not (assoc-ref create-feature-bounty-view-model-for-test "success?"))))
(define (then-bounty-is-in-persistence bounty)
(test-assert (member bounty bounties)))
(define (then-bounty-is-not-in-persistence bounty)
(test-assert (not (member bounty bounties))))
(define (then-failure-reason-is-provided reason)
(test-assert (assoc-ref create-feature-bounty-view-model-for-test "message")))
(define (clear-persistence)
(set! bounties '()))
(test-begin "create-bounties")
(define dummy-bounty
(make-feature-bounty
"dummy@email.net"
"dummy title"
"a dummy description for a dummy feature bounty"
"5"
#f))
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-success-is-reported)
(then-bounty-is-in-persistence dummy-bounty)
(clear-persistence)
(given-a-broken-repository)
(when-create-feature-bounty
(feature-bounty-email dummy-bounty)
(feature-bounty-title dummy-bounty)
(feature-bounty-description dummy-bounty)
(feature-bounty-reward dummy-bounty)
(feature-bounty-due-date dummy-bounty))
(then-creation-failure-is-reported)
(then-failure-reason-is-provided "Persistence is broken")
(then-bounty-is-not-in-persistence dummy-bounty)
(test-end "create-bounties")Là, le test échoue (les trois then). Pour le faire passer, il faut que
- l'interactor puisse retourner autre chose qu'un status à
'successdans sa réponse lorsqu'il y a un souci avec le repository ; - la réponse de l'interactore dispose d'un
messageet que le presenter soit capable de traiter ce message ;
;; src/use-cases/create-feature-bounty.scm
(define-module (src use-cases create-feature-bounty)
#:use-module (src entities)
#:use-module (srfi srfi-9)
#:use-module (ice-9 exceptions)
#:export (make-feature-bounty-repository
make-create-feature-bounty-interactor
make-create-feature-bounty-request
make-create-feature-bounty-response
create-feature-bounty-response-status
create-feature-bounty-response-message))
(define-record-type <create-feature-bounty-request>
(make-create-feature-bounty-request email title description reward due-date)
create-feature-bounty-request?
(email create-feature-bounty-request-email)
(title create-feature-bounty-request-title)
(description create-feature-bounty-request-description)
(reward create-feature-bounty-request-reward)
(due-date create-feature-bounty-request-due-date))
(define-record-type <create-feature-bounty-response>
(make-create-feature-bounty-response status message)
create-feature-bounty-response?
(status create-feature-bounty-response-status)
(message create-feature-bounty-response-message))
(define-record-type <feature-bounty-repository>
(make-feature-bounty-repository create)
feature-bounty-repository?
(create feature-bounty-repository-create))
(define (make-create-feature-bounty-interactor bounty-repository)
(lambda (request)
(let ((email (create-feature-bounty-request-email request))
(title (create-feature-bounty-request-title request))
(description (create-feature-bounty-request-description request))
(reward (create-feature-bounty-request-reward request))
(due-date (create-feature-bounty-request-due-date request)))
(guard
(ex
(else
(make-create-feature-bounty-response 'failure (exception-message ex))))
(begin
((feature-bounty-repository-create bounty-repository)
(make-feature-bounty email title description reward due-date))
(make-create-feature-bounty-response 'success #f))))));; tests/interface-adapters
(define-module (tests interface-adapters)
#:use-module (src entities)
#:use-module (src use-cases create-feature-bounty)
#:export (create-feature-bounty-presenter-for-test
create-feature-bounty-view-model-for-test
create-feature-bounty-controller-for-test
clear-create-feature-bounty-view-model-for-test))
(define create-feature-bounty-view-model-for-test '())
(define (create-feature-bounty-presenter-for-test response)
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"success?"
(if
(eq? 'success (create-feature-bounty-response-status response))
#t
#f)))
(set! create-feature-bounty-view-model-for-test
(assoc-set! create-feature-bounty-view-model-for-test
"message"
(create-feature-bounty-response-message response))))
(define (create-feature-bounty-controller-for-test interactor)
(lambda* (email title description reward #:optional due-date)
(create-feature-bounty-presenter-for-test
(interactor
(make-create-feature-bounty-request email title description reward due-date)))))Nous voilà à présent avec notre deuxième test qui passe ! C'est l'heure du réusinage.
Réusinage
Bon, je dois t'avouer quelque chose… Je me suis laissé prendre par une frénésie en plein réusinage et voilà où ça m'a mené :
- j'ai fait évoluer l'écriture des tests pour leur donner un style plus déclaratif.
- j'ai clarifier l'arborescence des tests pour leur donner une allure de Clean Architecture.
Voici la struture du projet :
├── src
│ ├── entities.scm
│ └── use-cases
│ └── create-feature-bounty.scm
└── tests
├── constants.scm
├── frameworks-and-drivers
│ ├── databases
│ │ └── in-memory.scm
│ └── my-framework
│ ├── assert.scm
│ ├── context.scm
│ └── runner.scm
├── helpers
│ └── then.scm
├── interface-adapters
│ ├── controller.scm
│ ├── presenter.scm
│ ├── repository.scm
│ └── view-model.scm
└── main.scmEt voici une archive qui contient tout ça pour pouvoir suivre le prochain chapitre : app-use-case-1.tar.gz