3T : les Tests en Trois Temps
Date de publication : 27 août 2011. Date de mise à jour : 21 octobre 2011.
Par
Thierry Leriche-Dessirier (www.icauda.com)
Le TDD, la fameuse méthode de Développement Guidé par les Tests est devenue incontournable. Toutefois, elle n'est
pas si simple à comprendre et à mettre en œuvre.
Ce mini-article propose, comme version allégée du TDD, la "3T" qui, bien qu'incomplète, devrait suffire à la
plupart des équipes.
19 commentaires
I. Introduction
II. Le cahier des charges
III. Mise en place
IV. 3T en action
IV-A. Interface
IV-B. Tests
IV-C. Développement
V. Les évolutions, bogues et mises à jour
VI. Suivi
VII. Conclusion
VIII. Remerciements
IX. Annexes
IX-A. Quelques réponses
IX-A-1. "3T" diverge pas mal du TDD.
IX-A-2. La méthode de calcul du factoriel proposée renvoie 0 quand N vaut 50.
IX-A-3. Tâche finie
I. Introduction
Aujourd'hui, tous les acteurs du développement sont d'accord sur l'importance des tests. La réalisation
d'une application passe nécessairement par des phases de tests. Une application non soumise aux tests
est une application qui par nature sera beaucoup plus instable et donc aura toutes les chances d'être vouée
à l'échec, l'abandon, ou au délaissement. Pourtant, encore trop souvent, les projets font l'impasse, espérant
gagner un peu de temps. Les tests sont accusés, bien à tort, de consommer trop de charges, d'être difficiles à
maintenir, etc. Or une stratégie dans laquelle les tests sont au cœur des développements peut faire gagner non
seulement en fiabilité ou en crédibilité mais aussi en temps.
Une des méthodes à la mode est la fameuse TDD (Test Driven Development) ou "Développement Guidé par
les Tests" qui ne sera pas expliquée ici. À la place je propose "3T", pour "Test en Trois Temps". Cette solution personnelle s'inspire des TDD. J'en propose une version allégée qui convient pour la plupart des besoins simples et, surtout, qui ne nécessite que très peu de temps pour la mettre en œuvre.
II. Le cahier des charges
À défaut de spécifications claires, bien rédigées, avec des règles métier correctement identifiées, il n'est
pas rare qu'on ne dispose que de simples post-it. Illustrons donc la suite de cet article à l'aide de post-it.
Dans la suite, et à titre d'illustration pour cet article, disons que le client demande le développement de la
fonction mathématique "Factoriel" dans la couche "Service" du projet. Il fournit la série de post-it suivante :
Regle #RG1.0
f(n) = 1 si n == 0
ie. 0! = 1
Regle #RG1.1
f(n) = 1 si n == 1
ie. 1! = 1
Regle #RG1.2
f(n) = n! = n * (n - 1)!
ex. 5! = 5 * 4 * 3 * 2 * 1 = 120
Regle #RG1.3
f(n) => Erreur (IllegalArgumentException) si n < 0
 |
Si, en tant que développeur, on découvre une incohérence dans le cahier des charges, on doit en avertir
le client. Il ne faut pas prendre l'initiative de modifier la demande sans l'accord du client. Dans un cas
plus complexe, il se peut qu'on ait simplement mal compris les spécifications.
|
III. Mise en place
Il faut avoir un projet sur lequel travailler. Pour illustrer cet article, on crée donc un miniprojet, avec
Maven, nommé "my-calculette" et dans lequel on programmera des fonctions mathématiques simples en guise d'exemple.
Sur le disque, on ajoute le dossier "my-calculette" (à l'emplacement que l'on souhaite) puis on y crée
le fichier "pom.xml" avec le contenu suivant :
| pom.xml |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.thi</groupId>
<artifactId>calculette</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>calculette</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
|
Ensuite, en ligne de commande, dans le répertoire "my-calculette", on fabrique la structure du projet Eclipse à
l'aide de la commande Maven suivante.
| Création du projet |
mvn clean install eclipse:eclipse
|
 |
Si on vient tout juste d'installer Maven, celui-ci va commencer par rapatrier un nombre assez important de
bibliothèques, dont JUnit, nécessaires à son fonctionnement. Donc, il est important d'avoir une connexion
Internet active à ce moment-là.
|
La commande crée en particulier le dossier "target" et les fichiers Eclipse ".classpath" et ".project". Il ne reste
plus qu'à importer le projet dans Eclipse (via le menu "File>Import>General>Existing Projects into Workspace"). Dans
le projet importé, on crée un répertoire source, via un clic droit sur le projet, puis "New>Source Folder".
Maintenant on peut travailler.
 |
Eclipse et Maven ne sont utilisés ici qu'à titre pratique. Ils n'ont rien à voir avec "3T".
|
IV. 3T en action
Maintenant qu'on a mis en place un projet Eclipse (my-calculette) et qu'on possède un
cahier des charges, on peut s'intéresser à "3T", en trois étapes :
- écriture des interfaces ;
- rédaction des tests ;
- développement assisté par les tests.
IV-A. Interface
Puisqu'il est demandé de programmer le calcul de la fonction Factoriel dans
la couche Service, on commence par déclarer cette méthode, de manière classique (et propre),
dans l'interface dédiée.
| L'interface |
public interface CalculetteService {
<br/>
@param
@return
@throws
int factoriel(int n);
...
}
|
IV-B. Tests
Avant même de coder l'implémentation du calcul, on écrit la classe de test.
Cette seconde étape illustre ce qu'est le TDD, en créant une rupture avec les
processus classiques de développement. On change ainsi d'approche, en mettant en avant le résultat à
obtenir, plutôt que de commencer à s'y intéresser une fois les développements terminés ou bien avancés.
Dans 3T cette seconde étape est relativement simple. En effet, il suffit de "recopier" les spécifications.
Afin d'assurer une bonne lisibilité et une bonne maintenabilité des tests, il y a deux principes qu'il
convient de suivre :
- renseigner la Javadoc de chaque méthode de test avec le résultat attendu ;
- avoir une structure homogène du corps de chaque méthode de test, afin de faire apparaître les paramètres,
les résultats attendus et les méthodes testées.
Voici un exemple de méthode de test :
| Avec tous les tests |
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_0() {
final int n = 0;
final int result = 1;
final int resultatCalcule = calc.factoriel(n);
assertEquals(result, resultatCalcule);
}
|
 |
Pour l'écriture d'une méthode de test, on se souvient de la règle des 3 A(s) :
Arrange (préparation des objets et mocks pour le test) puis Act (appel de la méthode à tester) et enfin
Assert (lignes d'assertions). Ici le premier "A" est composé des blocs "param" et "résultat attendu".
|
On reproduit ce schéma pour toutes les règles du cahier des charges. Comme les appels et les tests sont
globalement tous les mêmes, on peut en profiter pour les factoriser.
| Format |
public class CalculetteServiceTest {
protected CalculetteService calc;
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_0() {
final int n = 0;
final int result = 1;
doTestRG1_x(n, result);
}
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_1() {
final int n = 1;
final int result = 1;
doTestRG1_x(n, result);
}
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_2() {
final int n = 5;
final int result = 120;
doTestRG1_x(n, result);
}
<br/>
<br/>
<br/>
@Test(expected = IllegalArgumentException.class)
public void testCalcRG1_3() {
final int n = -3;
final int result = 1;
doTestRG1_x(n, result);
}
<br/>
<br/>
private void doTestRG1_x(final int n, final int result) {
final int resultatCalcule = calc.factoriel(n);
assertEquals(result, resultatCalcule);
}
}
public class RecursiveCalculetteServiceTest extends CalculetteServiceTest {
@Before
public void doBefore() {
}
}
|
 |
La classe CalculetteServiceTest doit être "abstraite" et permettre de faire des tests génériques quelle que
soit l'implémentation du service.
|
 |
Dans le cas de la règle RG1.2, on peut écrire plusieurs tests avec différentes valeurs de n, prises au hasard. Quand on souhaite
tester plusieurs valeurs, on peut soit regrouper les tests dans une seule méthode soit écrire une méthode pour chaque
valeur. C'est ce second choix que je propose dans le cadre de 3T. On aura ainsi un code ressemblant au suivant.
|
| Test de plusieurs valeurs |
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_2_120() {
final int n = 5;
final int result = 120;
doTestRG1_x(n, result);
}
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_2_24() {
final int n = 4;
final int result = 24;
doTestRG1_x(n, result);
}
<br/>
<br/>
<br/>
@Test
public void testCalcRG1_2_C() {
final int n = 6;
final int result = 720;
doTestRG1_x(n, result);
}
...
|
On lance la série de tests (bouton droit sur le nom de la classe, puis menu Run As > JUnit test) et on constate
que tous les tests sont rouges. C'est normal puisque la méthode "doBefore()" ne définit pas encore
d'implémentation. On n'obtient que des NullPointerException.
On va donc maintenant créer une première implémentation, qui ne fait que compiler.
| Implémentation minimale |
public class RecursiveCalculetteService implements CalculetteService {
@Override
public int factoriel(int n) {
throw new UnsupportedOperationException("Cette méthode n'a pas encore été écrite.");
}
}
|
Puis on finit la méthode "doBefore()" en utilisant cette implémentation.
| Set up |
public class RecursiveCalculetteServiceTest {
@Before
public void doBefore() {
calc = new RecursiveCalculetteService();
}
...
}
|
On relance les tests, qui restent rouges. Toutefois, la classe de tests est désormais finie et on
peut la mettre de côté.
IV-C. Développement
Dans cette troisième et dernière étape, il s'agit de programmer la fonctionnalité demandée à proprement
parler. Il faut procéder par itération, en s'aidant de la classe de tests qu'on a laissée ouverte dans un coin
d'Eclipse. L'idée est de n'écrire que du code pour faire passer les tests au vert, l'un après l'autre, un
seul à la fois. Le mieux est de le faire dans l'ordre des tests quand c'est possible.
On commence donc par la première règle : RG1.0
| RG1.0 |
@Override
public int factoriel(int n) {
if(n == 0) {
return 1;
}
throw new UnsupportedOperationException("Cette partie de la méthode n'a pas encore été écrite.");
}
|
On relance donc la série de tests et on constate que le test "testCalcRG1_0" devient vert, tandis que les
trois autres restent rouges.
On continue avec la règle suivante : RG1.1.
| RG1.1 |
@Override
public int factoriel(int n) {
if(n == 0 || n == 1) {
return 1;
}
throw new UnsupportedOperationException("Cette partie de la méthode n'a pas encore été écrite.");
}
|
On relance donc la série de tests et on constate que le test "testCalcRG1_1" devient vert, que le test
"testCalcRG1_0" reste vert, et que les 2 autres restent rouges.
On continue avec la règle suivante : RG1.2.
| RG1.2 |
@Override
public int factoriel(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factoriel(n - 1);
}
|
On relance donc la série de tests et on constate que le test "testCalcRG1_2" devient vert, que les tests
"testCalcRG1_1" et "testCalcRG1_0" restent verts, et que le dernier test reste rouge.
On continue avec la dernière règle : RG1.3.
| RG1.3 |
@Override
public int factoriel(int n) {
if(n < 0) {
throw new IllegalArgumentException("Le param n ne peut pas être négatif.");
}
if (n == 0 || n == 1) {
return 1;
}
return n * factoriel(n - 1);
}
|
On relance donc la série de tests et on constate que tous les tests sont désormais verts. À ce stade, on est en
droit de penser que (sauf hasard malheureux) le développement de la fonctionnalité est terminé.
Pour finir on lance l'ensemble des tests de l'application, et non pas seulement de
la classe (sous réserve que l'application contienne d'autres tests, ce qui devrait être le cas si le calcul du
Factoriel n'est pas la première fonctionnalité de la calculette, cf. Chapitre "V Les évolutions"), pour vérifier que le
développement n'a pas introduit de régression.
Si tous les tests sont verts, c'est qu'il n'y a rien à faire et on peut raisonnablement se dire que le développement
est terminé. Par contre, si un ancien test devient rouge, c'est qu'on a créé une régression, qu'il faut corriger.
 |
Si on dispose d'un serveur d'intégration, on peut se contenter de ne lancer les tests
que sur un sous-ensemble des tests de l'application pour vérifier la non-régression. En effet, le serveur
d'intégration se chargera alors de lancer l'intégralité des tests. L'idée est qu'il peut y avoir des milliers de
tests dans l'application et les lancer peut prendre beaucoup de temps.
|
V. Les évolutions, bogues et mises à jour
C'est inévitable, à un moment ou un autre, le client va commander des évolutions de l'application.
Voici un exemple d'évolution évidemment présenté sous forme de post-it.
Regle #RG1.2.1
Le calcul prend moins de 1 seconde.
À partir de là, on repart directement sur "3T".
Pour l'étape 1 (Interface) il n'y a rien à faire dans l'exemple.
Pour l'étape 2, il faut ajouter un nouveau test.
| RG1.2.1 |
<br/>
<br/>
<br/>
@Test(timeout=1000)
public void testCalcRG1_2_1() {
testCalcRG1_2();
}
|
Au départ, on s'intéresse uniquement au(x) nouveau(x) test(s) qu'on doit faire passer au vert. S'ils sont verts,
c'est qu'il n'y a rien à faire. Ici on ajoute une contrainte de performance donc c'est possible, mais pour
une nouvelle fonctionnalité, ça n'arrive jamais. Si un nouveau test est rouge, on passe à la troisième étape.
Pour l'étape 3 (nouveau dév), on programme la nouvelle fonctionnalité et on relance les nouveaux tests.
Quand les nouveaux tests sont verts, on peut considérer que les nouveaux développements (pris seuls) sont OK.
Une fois que les nouveaux tests sont verts, on relance l'ensemble des tests pour vérifier la non-régression.
 |
Quand un bogue est découvert, il donne généralement lieu à la création d'un ticket dans le système de gestion des
anomalies (Mantis, Jira, etc.) Le ticket est alors référencé par un numéro unique et peut faire office de
nouveau post-it. La correction d'un bogue peut se dérouler comme le développement d'une nouvelle fonctionnalité,
c'est-à-dire comme indiqué plus haut.
|
 |
Quand le cahier des charges évolue, et plus particulièrement quand une règle (déjà développée) change, je conseille
de ne pas versionner ladite règle mais d'en créer une nouvelle (avec un nouveau numéro). Si la nouvelle règle est
incompatible avec l'ancienne, on supprime simplement l'ancienne règle, devenue fausse.
|
VI. Suivi
Un point très intéressant de "3T" est le suivi (reporting) qu'on peut/doit mettre en place. En effet "3T"
propose de recopier les spécifications pour écrire les tests. Or les spécifications définissent très précisément
le contenu de l'application. Par association, lorsque N % des tests passent au vert, cela "signifie" que N % du
cahier des charges est satisfait, c'est-à-dire que l'application est développée à hauteur de N %.
Des logiciels, liés au serveur d'intégration, permettent de suivre l'avancement des développements et de donner
de la visibilité au chef de projet. En effet, le serveur d'intégration récupère la dernière version du code de
manière régulière (par exemple tous les soirs, ou après chaque commit), puis le compile et lance les tests. Le
chef de projet peut alors consulter les résultats.
VII. Conclusion
Sans suivre tous les principes du TDD, la "3T" permet de développer une application rapidement, avec un
taux de confiance élevé et en forçant les membres de l'équipe à documenter. La "3T" permet de couvrir
l'ensemble des spécifications à moindres frais et de façon relativement automatisée ; il suffit de recopier.
Cela évite notamment aux développeurs de se poser mille questions, dans tous les sens, et de produire exactement
ce qui est demandé, sans rien oublier ni ajouter en trop.
VIII. Remerciements
Je tiens à remercier, en tant qu'auteur de cet article de présentation de la "3T", toutes les personnes qui m'ont aidé
et soutenu. Je pense tout d'abord à mes collègues qui subissent mes questions au quotidien
mais aussi à mes contacts et amis du web, dans le domaine de l'informatique ou non, qui m'ont fait part de leurs
remarques et critiques. Bien entendu, je n'oublie pas l'équipe de developpez.com qui m'a guidé dans la rédaction de
cet article et m'a aidé à le corriger et le faire évoluer, principalement sur le forum.
Plus particulièrement j'adresse mes remerciements, par ordre alphabétique, à
Aldian,
ClaudeLELOUP,
Estelle,
Hing,
keulkeul,
Laurent.B,
Nemek,
et Yann
IX. Annexes
IX-A. Quelques réponses
IX-A-1. "3T" diverge pas mal du TDD.
Oui c'est exact et c'est assumé. Faire du Test Driven Dev, c'est se donner les moyens de réussir ses projets.
Néanmoins, peu de CP (Chef de Projet) sont prêts à se lancer dans l'aventure. La mise en œuvre de la "3T" ne nécessite
que très peu d'investissements. Le choix et l'écriture des cas de test est quasi systématique puisqu'il suffit
de recopier les spécifications. Certains outils (non présentés dans ce document) permettent même de générer du
code de test directement à partir du cahier des charges, par exemple depuis un Wiki. En outre, certains concepts
des TDD, notamment en ce qui concerne la manière de faire initialement échouer des tests, ne sont pas évidents à
comprendre. "3T" est alors un compromis qui semble cohérent dans de nombreux cas.
IX-A-2. La méthode de calcul du factoriel proposée renvoie 0 quand N vaut 50.
Oui et c'est normal. En Java, les "int" sont codés sur 32 bits. Les chiffres positifs vont de 0 à 2^31-1. Quand
on calcule "50!" on dépasse la capacité des "int". D'ailleurs on dépasse la capacité max dès que N vaut 28 ou
plus. On pourrait utiliser des "long", codés sur 64 bits mais ça ne ferait que reculer le problème. On pourrait
aussi utiliser des BigInteger, déjà mieux adaptés. Mais l'idéal est encore d'utiliser des techniques spécifiques
aux grands nombres. Toutefois ce n'est pas l'objet de ce document. La méthode de calcul du factoriel est ici
proposée à titre d'illustration.
IX-A-3. Tâche finie
Une des questions qu'on se pose souvent (ou qu'on devrait se poser) est de savoir sur quel(s) critère(s) tâche/fonctionnalité peut être considéré comme terminée. Dans le cadre de 3t, c'est relativement simple ; une tâche est terminée lorsqu'elle a franchi les trois phases (Interface, Test, Développement) décrites par la méthode.


Les sources présentées sur cette page sont libres de droits
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©
2011 Thierry Leriche-Dessirier. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu :
textes, documents, images, etc. sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.