programmez-en-d/tests_unitaires.whata

323 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = Tests unitaires
partAs = chapitre
translator = "Raphaël Jakse"
]
Les gens devraient, pour la plupart, en être conscients : toute machine contenant du code d'un programme contient des bogues logiciels. Les bogues logiciels menacent les systèmes informatiques du plus simple au plus complexe. Débogguer et corriger les bugs logiciels font partie des activités quotidiennes les moins agréables d'un programmeur.
[ = Causes des bogues
Il y a pleins de raisons pour lesquels les bogues logiciels existent. Ce qui suit en est une liste incomplète depuis de la conception d'un programme jusq'à la programmation elle-même :
- Les prés-requis et les spécifications du programme peuvent ne pas être clairs. Ce que le programme devrait vraiement faire peut ne pas être connu au moment de la conception.
- Le programmeur peut mal comprendre certains des pré-requis du programme.
- Le langage de programmation peut ne pas être suffisemment expressif. En considérant qu'il y a des confusions même entre deux humains parlant la même langue maternelle, la syntaxe et les règles non naturelles d'un langage de programmation peuvent être sources d'erreurs.
- certaines suppositions du programmeur peuvent être incorrectes. Par exemple, le programmeur peut supposer que [c 3.14] est assez précis pour représenter [m \pi].
- Le programmeur peut avoir une connaissance incorrecte sur un sujet ou même pas du tout. Par exemple, le programmeur peut ne pas savoir qu'utiliser une variable en point flottant dans une expression logique particulière n'est pas fiable.
- Le programme peut se trouver dans une situation imprévue. Par exemple, un des fichiers d'un répertoire peut être supprimé ou renommé pendant que le programme utilise les fichiers de ce répertoire dans une boucle [c foreach].
- Le programmeur peut faire des erreurs idiotes. Par exemple, le nom d'une variable peut être mal tapée et accidentellement correspondre au nom d'une autre variable.
- etc.
Malheureusement, il n'y a toujours pas de méthode de développement qui assure qu'un programme fonctionnera toujours correctement. Il s'agit toujours d'un sujet d'actualité en génie logiciel dans lequel des solutions prometteuses émergent tous les dix ans.
]
[ = Découvrir les bogues
Les bogues logiciels sont découverts à différents moment dans la vie du programme avec différents types d'outils ou de personnes. Ce qui suit est une liste partielle de moments pouvant correspondre à la découverte d'un bogue, du plus tôt au plus tard :
- Lors de l'écriture du programme
- par le programmeur
- par un autre programmeur pendant une session de [* programmation en binôme]
- par le compilateur, qui va afficher des messages lors de la compilation
- par les [** tests unitaires], lors de la construction du programme
- Lors de la relecture du code
- par les outils qui analysent le code lors de la compilation
- par d'autres programmeurs lors d'un [* audit du code]
- Lors de l'exécution du programme
- par des outils tels que Valgrind qui analysent l'exécution du programme
- pendant les tests de qualité, soit par l'échec d'une assertion ou par le comportement observé du programme
- par les utilisateurs des versions beta avant la sortie du programme
- par les utilisateurs finaux après la sortie du programme
Détecter le bug le plus tôt possible réduit les pertes d'argent, de temps et dans certains cas, de vies humaines. De plus, identifier les causes du bogues qui on été découvertes par les utilisateurs finaux est plus difficile qu'identifier les cause des bugs qui sont découverts plus tôt, pendant le développement.
]
[ = Le test unitaire pour déceler les bugs
Comme les programmeurs sont écrits par les programmeurs et que D est un langage compilé, les programmeurs et le compilateur seront toujours là pour découvrir les bogues. Ce qui les suit dans la vie du programme et (en partie pour cette raison) dans l'efficacité pour déceler les bogues est le test unitaire.
Le test unitaire constitue une partie indispensable de la programmation d'aujourd'hui. Il s'agit de la méthode la plus efficace pour limiter les erreurs de programmation. Selon certaines techniques de développement, un code auquel ne correspond aucun test unitaire est du code bogué.
Malheureusement, l'opposé n'est pas vrai : les tests unitaires ne garantissent pas que le code est libre de tout bogue. Même s'ils sont très efficace, il ne peuvent que réduire le risque de bogues.
Le test unitaire permet aussi la refactorisation du code (c.-à-d. l'améliorer) avec facilité et confiance. Autrement, il est courant de casser une fonctionnalité existante d'un programme lors d'un ajout de nouvelles fonctionnalités. Des bogues de ce type sont appelés [* régressions]. Sans test unitaire, les régressions ne sont des fois découvertes que lors des tests de qualité des nouvelles versions ou pire, par les utilisateurs finaux.
Le risque de régression découragent les programmeurs de refactoriser le code, ce qui les empêche parfois d'apporter la plus simple des améliorations comme corriger le nom d'une variable. Ceci entraîne le [* pourrissement du code], une situation dans laquelle le code devient de moins en moins maintenable. Par exemple, même si quelques lignes de code feraient mieux d'être bougées dans une nouvelle fonction pour être appelées depuis plusieurs endrois, la peur de créer des régressions fait que les programmeurs vont plutôt copier-coller les lignes, ce qui entraîne le problème de la [* duplication de code].
Les phrases du type « Si ce n'est pas cassé, ne le corrigez pas » viennent de la peur des régressions. Même si elles semblent transmettre la sagesse, de telles lignes de conduites entraînent, lentement mais sûrement, le pourrissement du code et celui-ci devient un bazar sans nom.
La programmation moderne rejette une telle « sagesse ». Au contraire, pour l'empêcher de devenir une source de bogues, le code est supposé être « refactorisé sans merci »? L'outil le plus important de cette approche moderne est le test unitaire.
Le test unitaire implique le test des plus petites unité de code indépendamment. Quand chaque unité de code est testé de façon indépendante, il est moins probable que des bogues apparaissent dans des codes de plus haut niveaux qui utilisent ces unités. Quand les parties fonctionnent correctement, il est plus probable que l'ensemble fonctionnera également.
Dans d'autres langages, les tests unitaires sont proposés par des bibliothèques (par ex. JUnit, CppUnit, Unittest++, etc). En D, le test unitaire est une fonctionnalité au cœur du langage. Qu'il soit préférable que la fonctionnalité fasse partie du cœur du langage est discutable. D ne proposant pas certaines fonctionnalités souvent trouvées dans les bibliothèques de test unitaire, il peut valoir la peine de considérer l'utilisation des bibliothèques.
Le test unitaire en D est aussi simple que d'insérer des assertions dans des blocs [c unittest].
]
[ = Activer les tests unitaires
Les tests unitaires ne font pas partie de l'exécution du programme. Ils ne devraient être activés que pendant le développement lorsqu'ils sont explicitement demandés.
The l'option du compilateur [c dmd] qui active les tests unitaires est [c -unittest]. (NdT: L'option équivalente pour [c gdc] est [c -funittest])
En supposant que le programme est écrit dans un seul fichier source nommé [c deneme.d], ses tests unitaires peuvent être activés par la commande suivante :
[code=bash <<<
dmd deneme.d -w -unittest
>>>]
Quand un programme qui est compilé avec les tests unitaires est démarré, ses blocs de tests unitaires sont d'abord exécutés. L'exécution du programme ne continue sur [c main()] que si tous les tests unitaires passent.
]
[ = Blocs [c unittest]
Les lignes de code qui impliquent les tests unitaires sont écrits dans les blocs [c unittest]. Ces blocs n'ont aucune autre signification pour le programme autre que contenir les tests unitaires :
[code=d <<<
unittest
{
/* ... Les tests et leur code support... */
}
>>>]
Même si les blocs [c unittest] peuvent apparaître n'importe où, il est pratique de les définir juste après le code qu'ils teste.
Par exemple, testons une fonction qui retourne la forme ordinale du nombre spécifié, comme dans "1er", "2nd", etc. Un bloc [c unittest] de cette fonction pourrait simplement contenir des instructions [c assert] qui comparent les valeurs de retours de la fonction aux valeurs attendues. La fonction suivante est testée avec les 3 formes de résultats de cette fonction :
[code=d <<<
string ordinal(size_t nombre)
{
// ...
}
unittest
{
assert(ordinal(1) == "1er");
assert(ordinal(2) == "2nd");
assert(ordinal(3) == "3ème");
}
>>>]
Les trois tests ci-dessus vérifient que la fonctionne correctement au moins pour les valeurs 1,2 et 3 en faisant 4 appels séparés à la fonction et en comparant les valeurs retournées aux valeurs attendues.
Même si les tests unitaires sont basés sur des assertions, les blocs [c unittest] peuvent contenir n'importe quel code D. Ceci permet des préparations avant de commencer les tests ou tout autre code support dont les tests pourraient avoir besoin. Par exemple, le bloc suivant définit d'abord une variable pour réduire la duplication de code.
[code=d <<<
dstring toFront(dstring str, in dchar letter)
{
// ...
}
unittest
{
immutable str = "hello"d;
assert(toFront(str, 'h') == "hello");
assert(toFront(str, 'o') == "ohell");
assert(toFront(str, 'l') == "llheo");
}
>>>]
Les trois assertions ci-avant vérifient que [c toFront()] est en accord avec sa spécification.
Comme ces exemples le montrent, les tests unitaires sont aussi utiles comme exemples d'utilisation des fonctions. Habituellement, il est facile de se faire une idée sur ce que fait une fonction simplement en regardant ses tests unitaires.
]
[ = Développement piloté par les tests (''Test Driven Development (TDD)'')
Le développement piloté par les tests est une méthode de développement logiciel qui prescrit l'écriture de tests unitaires avant d'implémenter une fonctionnalité. En TDD, on se concentre sur le test unitaire. Coder est une activité secondaire qui fait passer les tests.
En accord avec le TDD, la fonction [c ordinal] ci-avant peut d'abord être implémenté incorrectement de façon intentionnelle :
[code=d <<<
import std.string;
string ordinal(size_t nombre)
{
return ""; // ← intentionellement faux
}
unittest
{
assert(ordinal(1) == "1er");
assert(ordinal(2) == "2nd");
assert(ordinal(3) == "3ème");
}
void main()
{}
>>>]
Même si la fonction est évidemment fausse, l'étape suivante serait de lancer les tests unitaires décèlent effectivement les problèmes de la fonction :
[code <<<
$ dmd deneme.d -w -O -unittest
$ ./deneme
core.exception.AssertError@deneme.d(10): unittest failure
>>>]
La fonction ne devrait être implémentée qu'[* après] avoir vu l'échec, et seulement pour faire passer les tests. Voici une implémentation qui passe les tests :
[code=d <<<
import std.string;
string ordinal(size_t nombre)
{
string suffixe;
switch (nombre) {
case 1: suffixe = "er"; break;
case 2: suffixe = "nd"; break;
default: suffixe = "ème"; break;
}
return format("%s%s", nombre, suffixe);
}
unittest
{
assert(ordinal(1) == "1er");
assert(ordinal(2) == "2nd");
assert(ordinal(3) == "3ème");
}
void main()
{}
>>>]
Comme les implémentations ci-avant passe les tests unitaires, il y a des raisons de penser que la fonction [c ordinal] est correcte. Avec l'assurance que les tests apportent, l'implémentation de la fonction peut être changée de pleins de manières avec confiance.
[ = Les tests unitaires avant les corrections de bogues
Les tests unitaires ne sont pas la panacée ; il y aura toujours des bogues. Si un bogue est découvert pendant l'exécution du programme, cela peut être vu comme une indication que les tests unitaires sont incomplets. Pour cette raison, il est mieux de [* d'abord] écrire un test unitaire qui reproduit le bogue et seulement [* alors] de corriger le bogue et de passer le nouveau test.
Considérons la fonction suivante qui retourne l'orthographe de la forme ordinale d'un nombre donné en [c dstring], en anglais :
[code=d <<<
import std.exception;
import std.string;
dstring orthographeOrdinal(dstring nombre)
{
enforce(nombre.length, "Le nombre ne peut pas être vide");
dstring[dstring] exceptions = [
"one": "first", "two" : "second", "three" : "third",
"five" : "fifth", "eight": "eighth", "nine" : "ninth",
"twelve" : "twelfth"
];
dstring resultat;
if (nombre in exceptions) {
resultat = exceptions[nombre];
} else {
resultat = nombre ~ "th";
}
return resultat;
}
unittest
{
assert(orthographeOrdinal("one") == "first");
assert(orthographeOrdinal("two") == "second");
assert(orthographeOrdinal("three") == "third");
assert(orthographeOrdinal("ten") == "tenth");
}
void main()
{}
>>>]
La fonction fait attention aux exceptions orthographiques et inclue même un test unitaire pour cela. Cependant, la fonction a un bogue qui reste à découvrir :
[code=d <<<
import std.stdio;
void main()
{
writefln("He came the %s in the race.", // "il arriva %s de la course"
orthographeOrdinal("twenty")); // "vingt" - en anglais, "vingtième" se dit "twentieth".
}
>>>]
L'erreur d'orthographe dans la sortie du programme est due à un bogue dans [c orthographeOrdinal], que les tests unitaires ne décèlent malheureusement pas :
[output <<<
He came the twentyth in the race.
>>>]
Même s'il est facile de voir que la fonction ne produit pas l'orthographe correcte pour les nombres qui finissent par un « y », Le TTD prescrit l'écriture d'un test unitaire qui reproduit le bogue avant de le corriger :
[code=d <<<
unittest
{
// ...
assert(orthographeOrdinal("twenty") == "twentieth");
}
>>>]
Avec cet ajout aux tests, le bogue de la fonction est maintenant décelé pendant le développement :
[output <<<
core.exception.AssertError@deneme.d(33): unittest failure
>>>]
Et seulement alors, la fonction devrait être corrigée :
[code=d <<<
dstring orthographeOrdinal(dstring nombre)
{
// ...
if (nombre in exceptions) {
resultat = exceptions[nombre];
} else {
if (nombre[$-1] == 'y') {
resultat = nombre[0..$-1] ~ "ieth";
} else {
resultat = nombre ~ "th";
}
}
return resultat;
}
>>>]
]
]
[ = Exercice
Implémentez [c toFront()] en utilisant le TDD. Commencez par l'implémentation intentionnellement incomplète qui suit. Observez que les tests unitaires échouent et donnez une implémentation qui passe les tests.
[code=d <<<
dstring toFront(dstring str, in dchar lettre)
{
dstring resultat;
return resultat;
}
unittest
{
immutable str = "hello"d;
assert(toFront(str, 'h') == "hello");
assert(toFront(str, 'o') == "ohell");
assert(toFront(str, 'l') == "llheo");
}
void main()
{}
>>>]
[[part:corrections/tests_unitaires | … La solution]]
]