Bonjour. Dans cette séquence on va voir le framework de
test, SUnit en Pharo, qui permet d'écrire des tests
unitaires comme on va le voir.
Un framework de test sert à supporter le développement agile.
L'idée, c'est de faire du développement incrémental et de
tester régulièrement son code pour voir si quand on l'a
modifié, on n'a pas cassé une propriété ou un invariant
quelque part dans le code. L'idée, c'est d'être préparé au changement.
On écrit des tests et ensuite on modifie son code, puis
on va être capable de réexécuter les tests pour voir si
on n'a pas cassé ou modifié quelque chose qu'il n'aurait pas fallu.
Avoir des tests automatisés c'est extrêmement important
pour supporter ce type de programmation.
Le framework SUnit est un framework spécialisé pour l'écriture
de tests, qui en facilite l'écriture, il est composé de 4
classes donc il est vraiment très simple.
Originellement il a été développé par Kent Beck.
Il a été l'inspiration de nombreux frameworks de tests
dans d'autres langages, comme J-Unit, par exemple.
Qu'est-ce qu'un test ?
Quand on écrit un test on effectue 3 étapes.
Première étape, on met en place un contexte, par exemple
je crée un set vide.
Deuxième étape je crée un stimulus, ici, j'essaie d'insérer
2 fois un élément dans le set que je viens de créer.
Troisième étape, je teste le résultat que j'obtiens, c'est-à-dire
que je m'attends à ce que le set ne contienne qu'un seul
élément, car je vous rappelle un objet set ne peut
contenir qu'une seule fois un élément, il ne peut pas le contenir 2 fois.
Je teste le résultat en espérant que l'invariant n'est pas cassé.
Je vous montre l'exemple.
Pour écrire un test, il faut que j'écrive une sous-classe
de TestCase, en l'occurrence ici je l'appelle SetTestCase.
C'est pour tester les sets.
Je définis une méthode qui s'appelle testAdd.
On verra que toutes les méthodes commencent par test.
Et puis, je mets en place le contexte.
Je crée une instance de la classe set qui est vide.
Je tente
d'ajouter 2 fois cet élément dans mon ensemble, donc 2
fois l'élément 5, ici et ici.
Et in fine, je teste, donc là j'ai un check.
J'utilise la méthode assert pour ça ou je regarde que la
taille de mon set est bien 1, que j'ai bien réussi à
ajouter qu'une seule fois l'élément.
Je peux lancer le test grâce à cette expression
et donc mon test sera vert si l'invariant, si le assert,
si l'expression qui est passée ici est vraie.
Toutes les méthodes qui commencent par la chaîne de
caractères test représentent un test et seront
automatiquement exécutées par l'outil de testRunner.
On verra que tous les résultats, donc toute l'exécution
de la méthode test produit un résultat.
Ces résultats y sont agrégés tous et ils sont agrégés au
sein d'un objet instance de la classe testResult.
Je vous donne un autre exemple.
Dans cet exemple on a bien la méthode de test, son nom
commence par est en minuscule. Donc ici, c'est le nom de
cette méthode, AdjacentRunsWithEqualsAttributAreMerged.
On a le contexte, ici, qui est posé.
On a créé un objet.
Ici, on a un stimuli.
On a tenté d'envoyer le message AddLastTimes à cet objet
3 fois, avec une première fois des paramètres qui sont
ici et une deuxième fois les mêmes paramètres, en
deuxième et troisième fois. On teste, in fine, ici c'est
le check, que cet élément est bien de taille 2.
On n'a pas pu ajouter plusieurs fois le même élément de façon adjacente.
Dans l'exécution d'un test, il y a plusieurs cas de
figure qui peuvent se produire. Il y a un cas de figure
qui correspond à ce qu'on appelle une failure, c'est-à-dire
c'est une des assertions, un invariant qu'on pensait vrai
ou qui devrait être vrai mais qui est faux.
Dans ce cas-là, le test, on dit qu'il contient
une failure. C'est un problème qui est anticipé.
On s'attendait à ce que, potentiellement, cette erreur
soit présente.
Et ensuite, une erreur c'est une condition qu'on n'a même pas testée.
En fait, c'est quelque chose qui se produit, par exemple
une exception qui est levée et cetera auquel on ne s'attendait
même pas au moment de l'écriture du test.
Ce sont 2 cas bien distincts.
Comment est-ce qu'on fait dans un test lorsqu'on veut
tester qu'un bout de code lève une
exception. Par exemple, je veux tester que set new remove
1, donc ce bout de code va lever l'exception not found,
puisque si je fais Set new, c'est forcément un ensemble
vide, donc j'essaye de retirer un élément d'un ensemble
vide, ça n'a pas de sens. Donc, ça va lever une exception not found.
Dans mon test j'utilise should raise.
Je passe un block. Au moment de l'évaluation de block, si
une exception est levée et que l'exception c'est bien Not
Found, alors le test sera vert, sera OK.
Je peux aussi tester l'inverse, tester qu'un morceau de
code ne lève pas d'exception. Ici, j'utilise la méthode self shouldnt raise.
Ce morceau de code ne doit pas lever l'exception NotFound.
On peut au moment de l'écriture de plusieurs tests, quand
on en écrit plein, se rendre compte qu'on a des doublons
au moment de l'écriture du contexte du test.
Par exemple, ici j'écris un autre test pour les sets.
Donc, testOccurrence et on voit qu'ici, dans le contexte,
je vais recréer encore un nouveau set vide.
A chaque fois que je vais écrire des tests pour un set,
je vais à chaque fois faire Set new au début dans le contexte.
Ce qu'on aimerait, c'est ne pas répéter à chaque fois
cette ligne dans tous nos tests.
Pour ne pas avoir à les répéter, pour pouvoir factoriser
ça quelque part ailleurs, on a une solution.
La solution en S-unit, c'est d'utiliser les méthodes
setUp pour factoriser toutes les initialisations
avant l'exécution d'un test.
En fait, ce qui se passe, c'est qu'au moment de l'exécution
d'un test, juste avant qu'une méthode qui commence par la
chaîne test soit exécutée, on va déclencher l'exécution
de la méthode Set up, poser le contexte, une bonne fois.
On va réaliser, pendant le test, les stimulis et le
check, les assertions et à la fin de l'exécution du test,
qu'il ait échoué ou pas d'ailleurs, on va exécuter la
méthode #tearDown qui va permettre de faire le nettoyage
de toutes les ressources qui devraient être libérées par exemple.
Si on regarde l'exécution de plusieurs méthodes tests, c'est facile.
On va avoir l'exécution de Set up, une première méthode
test qui est exécutée ici.
L'exécution tearDown pour nettoyer, une nouvelle
exécution de Set up, l'exécution d'un nouveau test,
tearDown SetUp, l'exécution d'un test et tearDown.
Ça nous permet de factoriser la mise en place du contexte
et le nettoyage des ressources dans 2 méthodes
particulières, setUp et tearDown.
A quoi ça ressemble ?
Dans notre exemple, si je prends le setTestCase, je peux
mettre en place définir la méthode setUp dans lequel j'écris
empty egal set new. Donc, empty devient une variable d'instance
de la classe set test case.
Et puis dans ma méthode de test, je peux directement
utiliser la variable d'instance empty qui a été
correctement initialisée, puisque avant l'exécution de la
méthode testOccurence la méthode setUp a été
exécutée.
Si on regarde l'organisation des classes au sein du coeur
de SUnit, comme je disais, il n'y a pas beaucoup de classes, il y en 4.
Donc un TestCase, ce n'est plus ni moins qu'un test qui
vérifie que certaines conditions sont vraies dans un
contexte donné, donc il y a un testCase il a une méthode
setUp, une méthode tearDown et puis un ensemble de méthode test.
On écrira tout le temps des sous-classes de la classe testCase.
Ces TestCase sont agrégés dans une
testSuite, dans une suite de test, et on peut
lancer l'exécution d'une suite complète.
Quand on lance l'exécution d'une suite, on obtient un
résultat et ce résultat est une instance de TestResult,
ici, qui nous dit combien de tests ont passé,
combien de tests ont été exécutés, combien de tests ont
potentiellement rencontré des failures et des erreurs.
On a aussi la notion de testResource qui permet de
définir des ressources pour tout un testSuite.
Le testCase, comme je le disais, ça représente un test, c'est
une méthode qui commence par test définie sur une sous-classe de testCase.
Une testSuite, c'est un groupe de test, ce sont toutes
les méthodes test case définies dans une classe, voire plusieurs classes.
Et puis un tesResult ça va être un résultat de l'exécution
de plusieurs tests.
Une ressource est un objet qui va permettre d'
initialiser un ensemble de ressources, qui sont coûteuses
à initialiser en temps normal, et qu'on ne veut
initialiser qu'une seule fois pour un ensemble de tests.
On met en place un testresource, on l'initialise une
fois, on l'exécute plein de tests et ensuite on pourra la libérer à la fin.
Ce qu'il faut retenir, c'est comment est-ce que vous
pouvez écrire des tests. Ecrire des tests, c'est
extrêmement simple, il suffit d'écrire une sous-classe de
la classe testCase, définir des méthodes sur celle-ci qui
commencent par test, et puis à l'intérieur mettre en
place un contexte, envoyer les stimulis et tester les
assertions qui devraient être vraies.
On veut réutiliser les contextes.
Pour les réutiliser à travers plusieurs méthodes test, on
les factorise dans une méthode setUp par exemple.
En résumé, dans cette séquence on a vu le framework de
test SUnit, qui est extrêmement simple à utiliser et
extrêmement efficace pour mettre en place du développement agile.
Je vous encourage vivement à l'utiliser.
Définir des tests, c'est extrêmement facile.
Le gros avantage, c'est qu'on peut exécuter, une fois qu'on
l'a écrit une fois, des millions de fois le même test et
c'est extrêmement pratique pour être sûr que son code
fonctionne encore, même si on a modifié des choses et
qu'il y a des effets de bord qui se sont produits, on
peut les détecter si on a été assez couvrant dans les tests qu'on a écrit.
On peut aller plus loin sur l'écriture de test en
utilisant des dot frameworks, typiquement des frameworks
de Mock comme BabyMock, et caetera, pour avoir d'autres
styles de tests ou d'écritures de tests.
Je vous encourage à utiliser de nombreux tests et à
écrire de nombreux tests dans vos programmes.