IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

45. Tests unitaires

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éboguer et corriger les bogues logiciels font partie des activités quotidiennes les moins agréables d'un programmeur.

45-1. Causes des bogues

Il y a plein de raisons pour lesquelles les bogues logiciels existent. Ce qui suit en est une liste incomplète depuis de la conception d'un programme jusqu'à la programmation elle-même :

  • les prérequis et les spécifications du programme peuvent ne pas être clairs. Ce que le programme devrait vraiment 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 suffisamment 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 source d'erreurs ;
  • certaines suppositions du programmeur peuvent être incorrectes. Par exemple, le programmeur peut supposer que 3.14 est assez précis pour représenter kitxmlcodeinlinelatexdvp\pifinkitxmlcodeinlinelatexdvp ;
  • 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 foreach ;
  • le programmeur peut faire des erreurs idiotes. Par exemple, le nom d'une variable peut être mal écrit 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.

45-2. Découvrir les bogues

Les bogues logiciels sont découverts à différents moments 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 bêta avant la sortie du programme,
    • par les utilisateurs finals après la sortie du programme.

Détecter le bogue 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 bogue qui a été découvert par les utilisateurs finals est plus difficile qu'identifier les causes des bogues qui sont découverts plus tôt, pendant le développement.

45-3. Le test unitaire pour déceler les bogues

Comme les programmes 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. Par ces deux aspects et pour ces raisons, les tests unitaires sont le meilleur moyen d'intercepter les bogues. .

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 efficaces, ils 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 parfois découvertes que lors des tests de qualité des nouvelles versions ou pire, par les utilisateurs finals.

Le risque de régression décourage 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 c'est préférable que quelques lignes de code soient logées dans une nouvelle fonction qui serait appelée depuis plusieurs endroits, 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 conduite 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és de code indépendamment. Quand chaque unité de code est testée de façon indépendante, il est moins probable que des bogues apparaissent dans des codes de plus haut niveau 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 unittest.

45-4. 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.

L'option du compilateur dmd qui active les tests unitaires est -unittest. (NDT L'option équivalente pour gdc est -funittest.)

En supposant que le programme est écrit dans un seul fichier source nommé deneme.d, ses tests unitaires peuvent être activés par la commande suivante :

 
Sélectionnez
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 main() que si tous les tests unitaires passent.

45-5. Blocs unittest

Les lignes de code qui impliquent les tests unitaires sont écrites dans les blocs unittest. Ces blocs n'ont aucune autre signification pour le programme que de contenir les tests unitaires :

 
Sélectionnez
unittest
{
    /* ... Les tests et leur code support... */
}

Même si les blocs unittest peuvent apparaître n'importe où, il est pratique de les définir juste après le code qu'ils testent.

Par exemple, testons une fonction qui retourne la forme ordinale du nombre spécifié, comme dans « 1er », « 2e », etc. Un bloc unittest de cette fonction pourrait simplement contenir des instructions assert qui comparent les valeurs de retour de la fonction aux valeurs attendues. La fonction suivante est testée avec les trois formes de résultats de cette fonction :

 
Sélectionnez
string ordinal(size_t nombre)
{
    // ...
}
 
unittest
{
    assert(ordinal(1) == "1er");
    assert(ordinal(2) == "2e");
    assert(ordinal(3) == "3e");
}

Les trois tests ci-dessus vérifient que la fonction fonctionne correctement au moins pour les valeurs 1,2 et 3 en faisant trois 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 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.

 
Sélectionnez
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 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.

45-6. 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 ordinal ci-avant peut d'abord être implémentée incorrectement de façon intentionnelle :

 
Sélectionnez
import std.string;
 
string ordinal(size_t nombre)
{
    return "";    // ← intentionnellement faux
}
 
unittest
{
    assert(ordinal(1) == "1er");
    assert(ordinal(2) == "2e");
    assert(ordinal(3) == "3e");
}
 
void main()
{}

Même si la fonction est évidemment fausse, l'étape suivante serait de lancer les tests unitaires décelant effectivement les problèmes de la fonction :

 
Sélectionnez
$ 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 :

 
Sélectionnez
import std.string;
 
string ordinal(size_t nombre)
{
    string suffixe;
 
    switch (nombre) {
    case  1: suffixe = "er"; break;
    case  2: suffixe = "e"; break;
    default: suffixe = "e"; break;
    }
 
    return format("%s%s", nombre, suffixe);
}
 
unittest
{
    assert(ordinal(1) == "1er");
    assert(ordinal(2) == "2e");
    assert(ordinal(3) == "3e");
}
 
void main()
{}

Comme les implémentations ci-avant passent les tests unitaires, il y a des raisons de penser que la fonction ordinal est correcte. Avec l'assurance que les tests apportent, l'implémentation de la fonction peut être changée avec confiance.

45-6-1. 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 dstring, en anglais :

 
Sélectionnez
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 inclut même un test unitaire pour cela. Cependant, la fonction a un bogue qui reste à découvrir :

 
Sélectionnez
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".
}

La faute d'orthographe dans la sortie du programme est due à un bogue dans orthographeOrdinal, que les tests unitaires ne décèlent malheureusement pas :

 
Sélectionnez
He came the twentyth in the race.

Même s'il est facile de voir que la fonction ne produit pas la syntaxe 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 :

 
Sélectionnez
unittest
{
// ...
    assert(orthographeOrdinal("twenty") == "twentieth");
}

Avec cet ajout aux tests, le bogue de la fonction est maintenant décelé pendant le développement :

 
Sélectionnez
core.exception.AssertError@deneme.d(33): unittest failure

Et seulement alors, la fonction devrait être corrigée :

 
Sélectionnez
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;
}

45-7. Exercice

Implémentez 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.

 
Sélectionnez
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()
{}

SolutionTests Unitaires - Correction.


précédentsommairesuivant