Tests unitaires - Unit testing

En programmation informatique , les tests unitaires sont une méthode de test de logiciels par laquelle des unités individuelles de code source ( ensembles d'un ou plusieurs modules de programme informatique avec les données de contrôle, les procédures d' utilisation et les procédures d'exploitation associées) sont testées pour déterminer si elles sont adaptées à l'utilisation. .

La description

Les tests unitaires sont généralement des tests automatisés écrits et exécutés par des développeurs de logiciels pour s'assurer qu'une section d'une application (appelée « unité ») respecte sa conception et se comporte comme prévu. En programmation procédurale , une unité peut être un module entier, mais il s'agit plus généralement d'une fonction ou d'une procédure individuelle. Dans la programmation orientée objet , une unité est souvent une interface entière, telle qu'une classe ou une méthode individuelle. En écrivant d'abord des tests pour les plus petites unités testables, puis les comportements composés entre celles-ci, on peut créer des tests complets pour des applications complexes.

Pour isoler les problèmes qui peuvent survenir, chaque scénario de test doit être testé indépendamment. Des substituts tels que des stubs de méthode , des objets fictifs , des contrefaçons et des faisceaux de test peuvent être utilisés pour aider à tester un module isolément.

Pendant le développement, un développeur de logiciel peut coder des critères ou des résultats connus pour être bons dans le test pour vérifier l'exactitude de l'unité. Pendant l'exécution du scénario de test, les frameworks enregistrent les tests qui échouent à un critère et les rapportent dans un résumé. Pour cela, l'approche la plus couramment utilisée est test - fonction - valeur attendue.

L'écriture et la maintenance des tests unitaires peuvent être rendues plus rapides en utilisant des tests paramétrés . Ceux-ci permettent l'exécution d'un test plusieurs fois avec différents ensembles d'entrées, réduisant ainsi la duplication du code de test. Contrairement aux tests unitaires traditionnels, qui sont généralement des méthodes fermées et testent des conditions invariantes, les tests paramétrés prennent n'importe quel ensemble de paramètres. Les tests paramétrés sont pris en charge par TestNG , JUnit et son homologue .Net, XUnit . Les paramètres appropriés pour les tests unitaires peuvent être fournis manuellement ou, dans certains cas, sont générés automatiquement par le cadre de test. Ces dernières années, la prise en charge a été ajoutée pour l'écriture de tests (unités) plus puissants, tirant parti du concept de théories, de cas de test qui exécutent les mêmes étapes, mais en utilisant des données de test générées au moment de l'exécution, contrairement aux tests paramétrés classiques qui utilisent les mêmes étapes d'exécution avec des ensembles d'entrée qui sont prédéfinis.

Avantages

Le but des tests unitaires est d'isoler chaque partie du programme et de montrer que les parties individuelles sont correctes. Un test unitaire fournit un contrat écrit strict auquel le morceau de code doit satisfaire. En conséquence, il offre plusieurs avantages.

Les tests unitaires détectent les problèmes au début du cycle de développement . Cela inclut à la fois les bogues dans l'implémentation du programmeur et les défauts ou les parties manquantes de la spécification de l'unité. Le processus d'écriture d'un ensemble complet de tests oblige l'auteur à réfléchir aux entrées, aux sorties et aux conditions d'erreur, et ainsi à définir plus précisément le comportement souhaité de l'unité. Le coût de recherche d'un bogue avant le début du codage ou lorsque le code est écrit pour la première fois est considérablement inférieur au coût de détection, d'identification et de correction du bogue plus tard. Les bogues dans le code publié peuvent également causer des problèmes coûteux pour les utilisateurs finaux du logiciel. Le code peut être impossible ou difficile à tester unitairement s'il est mal écrit, ainsi les tests unitaires peuvent forcer les développeurs à mieux structurer les fonctions et les objets.

Dans le développement piloté par les tests (TDD), qui est fréquemment utilisé à la fois dans la programmation extrême et dans Scrum , les tests unitaires sont créés avant que le code lui-même ne soit écrit. Lorsque les tests réussissent, ce code est considéré comme complet. Les mêmes tests unitaires sont fréquemment exécutés sur cette fonction, car la base de code plus large est développée soit au fur et à mesure que le code est modifié ou via un processus automatisé avec la construction. Si les tests unitaires échouent, cela est considéré comme un bogue soit dans le code modifié, soit dans les tests eux-mêmes. Les tests unitaires permettent alors de localiser facilement la localisation du défaut ou de la panne. Étant donné que les tests unitaires alertent l'équipe de développement du problème avant de transmettre le code aux testeurs ou aux clients, les problèmes potentiels sont détectés tôt dans le processus de développement.

Les tests unitaires permettent au programmeur de refactoriser le code ou de mettre à niveau les bibliothèques système à une date ultérieure, et de s'assurer que le module fonctionne toujours correctement (par exemple, dans les tests de régression ). La procédure consiste à écrire des cas de test pour toutes les fonctions et méthodes afin que chaque fois qu'un changement provoque une erreur, il puisse être rapidement identifié. Les tests unitaires détectent les changements qui peuvent rompre un contrat de conception .

Les tests unitaires peuvent réduire l' incertitude dans les unités elles - mêmes et peuvent être utilisés dans un bottom-up approche de style de test. En testant d'abord les parties d'un programme, puis en testant la somme de ses parties, les tests d'intégration deviennent beaucoup plus faciles.

Les tests unitaires fournissent une sorte de documentation vivante du système. Les développeurs qui cherchent à savoir quelles fonctionnalités sont fournies par une unité et comment l'utiliser peuvent consulter les tests unitaires pour acquérir une compréhension de base de l'interface de l'unité ( API ).

Les cas de test unitaires incarnent des caractéristiques essentielles au succès de l'unité. Ces caractéristiques peuvent indiquer une utilisation appropriée/inappropriée d'une unité ainsi que des comportements négatifs qui doivent être piégés par l'unité. Un cas de test unitaire, en soi, documente ces caractéristiques critiques, bien que de nombreux environnements de développement logiciel ne reposent pas uniquement sur le code pour documenter le produit en développement.

Lorsque le logiciel est développé à l'aide d'une approche pilotée par les tests, la combinaison de l'écriture du test unitaire pour spécifier l'interface et des activités de refactorisation effectuées après la réussite du test peut remplacer la conception formelle. Chaque test unitaire peut être considéré comme un élément de conception spécifiant des classes, des méthodes et un comportement observable.

Limites et inconvénients

Le test ne détectera pas toutes les erreurs du programme, car il ne peut évaluer tous les chemins d'exécution dans aucun des programmes les plus triviaux. Ce problème est un sur-ensemble du problème de l' arrêt , qui est indécidable . Il en est de même pour les tests unitaires. De plus, les tests unitaires, par définition, ne testent que la fonctionnalité des unités elles-mêmes. Par conséquent, il ne détectera pas les erreurs d'intégration ou les erreurs plus larges au niveau du système (telles que les fonctions exécutées sur plusieurs unités ou les zones de test non fonctionnelles telles que les performances ). Les tests unitaires doivent être effectués conjointement avec d'autres activités de test de logiciels , car ils ne peuvent montrer que la présence ou l'absence d'erreurs particulières ; ils ne peuvent prouver une absence totale d'erreurs. Pour garantir un comportement correct pour chaque chemin d'exécution et chaque entrée possible, et assurer l'absence d'erreurs, d'autres techniques sont nécessaires, à savoir l'application de méthodes formelles pour prouver qu'un composant logiciel n'a pas de comportement inattendu.

Une hiérarchie élaborée de tests unitaires n'équivaut pas à des tests d'intégration. L'intégration avec des unités périphériques doit être incluse dans les tests d'intégration, mais pas dans les tests unitaires. Les tests d'intégration reposent généralement encore fortement sur les tests manuels effectués par des humains ; les tests de haut niveau ou de portée mondiale peuvent être difficiles à automatiser, de sorte que les tests manuels semblent souvent plus rapides et moins chers.

Le test logiciel est un problème combinatoire. Par exemple, chaque instruction de décision booléenne nécessite au moins deux tests : un avec un résultat « vrai » et un avec un résultat « faux ». En conséquence, pour chaque ligne de code écrite, les programmeurs ont souvent besoin de 3 à 5 lignes de code de test. Cela prend évidemment du temps et son investissement peut ne pas en valoir la peine. Il existe des problèmes qui ne peuvent pas être facilement testés, par exemple ceux qui ne sont pas déterministes ou qui impliquent plusieurs threads . De plus, le code d'un test unitaire est susceptible d'être au moins aussi bogué que le code qu'il teste. Fred Brooks dans The Mythical Man-Month cite : « Ne partez jamais en mer avec deux chronomètres ; prenez-en un ou trois. » Autrement dit, si deux chronomètres se contredisent, comment savoir lequel est correct ?

Un autre défi lié à la rédaction des tests unitaires est la difficulté de mettre en place des tests réalistes et utiles. Il est nécessaire de créer des conditions initiales pertinentes pour que la partie de l'application testée se comporte comme une partie du système complet. Si ces conditions initiales ne sont pas définies correctement, le test n'exercera pas le code dans un contexte réaliste, ce qui diminue la valeur et la précision des résultats des tests unitaires.

Pour obtenir les avantages escomptés des tests unitaires, une discipline rigoureuse est nécessaire tout au long du processus de développement logiciel. Il est essentiel de conserver soigneusement des enregistrements non seulement des tests effectués, mais également de toutes les modifications apportées au code source de cette unité ou de toute autre unité du logiciel. L'utilisation d'un système de contrôle de version est indispensable. Si une version ultérieure de l'unité échoue à un test particulier qu'elle avait précédemment réussi, le logiciel de contrôle de version peut fournir une liste des modifications du code source (le cas échéant) qui ont été appliquées à l'unité depuis cette date.

Il est également essentiel de mettre en œuvre un processus durable pour garantir que les échecs des cas de test sont examinés régulièrement et traités immédiatement. Si un tel processus n'est pas mis en œuvre et ancré dans le flux de travail de l'équipe, l'application évoluera en désynchronisation avec la suite de tests unitaires, augmentant les faux positifs et réduisant l'efficacité de la suite de tests.

Les tests unitaires des logiciels système embarqués présentent un défi unique : étant donné que le logiciel est développé sur une plate-forme différente de celle sur laquelle il sera finalement exécuté, vous ne pouvez pas exécuter facilement un programme de test dans l'environnement de déploiement réel, comme cela est possible avec les programmes de bureau.

Les tests unitaires ont tendance à être plus faciles lorsqu'une méthode a des paramètres d'entrée et une sortie. Il n'est pas aussi facile de créer des tests unitaires lorsqu'une fonction majeure de la méthode est d'interagir avec quelque chose d'extérieur à l'application. Par exemple, une méthode qui fonctionnera avec une base de données peut nécessiter la création d'une maquette d'interactions de base de données, qui ne sera probablement pas aussi complète que les interactions de base de données réelles.

Exemple

Voici un ensemble de cas de test en Java qui spécifient un certain nombre d'éléments de l'implémentation. Premièrement, qu'il doit y avoir une interface appelée Adder et une classe d'implémentation avec un constructeur sans argument appelé AdderImpl. Il va à affirmer que l'interface Adder doit avoir une méthode appelée ajouter, avec deux paramètres entiers, qui renvoie un autre entier. Elle spécifie également le comportement de cette méthode pour une petite plage de valeurs sur un certain nombre de méthodes d'essai.

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class TestAdder {

    @Test
    public void testSumPositiveNumbersOneAndOne() {
        Adder adder = new AdderImpl();
        assertEquals(2, adder.add(1, 1));
    }

    // can it add the positive numbers 1 and 2?
    @Test
    public void testSumPositiveNumbersOneAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(3, adder.add(1, 2));
    }

    // can it add the positive numbers 2 and 2?
    @Test
    public void testSumPositiveNumbersTwoAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(4, adder.add(2, 2));
    }

    // is zero neutral?
    @Test
    public void testSumZeroNeutral() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(0, 0));
    }

    // can it add the negative numbers -1 and -2?
    @Test
    public void testSumNegativeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(-3, adder.add(-1, -2));
    }

    // can it add a positive and a negative?
    @Test
    public void testSumPositiveAndNegative() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(-1, 1));
    }

    // how about larger numbers?
    @Test
    public void testSumLargeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(2222, adder.add(1234, 988));
    }
}

Dans ce cas, les tests unitaires, ayant été écrits en premier, agissent comme un document de conception spécifiant la forme et le comportement d'une solution souhaitée, mais pas les détails de mise en œuvre, qui sont laissés au programmeur. Après la pratique "faire la chose la plus simple qui puisse fonctionner", la solution la plus simple qui fera réussir le test est indiquée ci-dessous.

interface Adder {
    int add(int a, int b);
}
class AdderImpl implements Adder {
    public int add(int a, int b) {
        return a + b;
    }
}

En tant que spécifications exécutables

L'utilisation des tests unitaires comme spécification de conception présente un avantage significatif par rapport aux autres méthodes de conception : le document de conception (les tests unitaires eux-mêmes) peut lui-même être utilisé pour vérifier la mise en œuvre. Les tests ne réussiront jamais à moins que le développeur ne mette en œuvre une solution conforme à la conception.

Les tests unitaires manquent de l'accessibilité d'une spécification schématique telle qu'un diagramme UML , mais ils peuvent être générés à partir du test unitaire à l'aide d'outils automatisés. La plupart des langages modernes ont des outils gratuits (généralement disponibles sous forme d'extensions aux IDE ). Des outils gratuits, comme ceux basés sur le framework xUnit , sous-traitent à un autre système le rendu graphique d'une vue pour la consommation humaine.

Applications

Programmation extrême

Les tests unitaires sont la pierre angulaire de la programmation extrême , qui repose sur un cadre de tests unitaires automatisé . Ce cadre de test unitaire automatisé peut être soit un tiers, par exemple xUnit , soit créé au sein du groupe de développement.

La programmation extrême utilise la création de tests unitaires pour le développement piloté par les tests . Le développeur écrit un test unitaire qui expose soit une exigence logicielle, soit un défaut. Ce test échouera soit parce que l'exigence n'est pas encore implémentée, soit parce qu'elle expose intentionnellement un défaut dans le code existant. Ensuite, le développeur écrit le code le plus simple pour faire réussir le test, ainsi que d'autres tests.

La plupart du code dans un système est testé unitairement, mais pas nécessairement tous les chemins à travers le code. La programmation extrême impose une stratégie de « tester tout ce qui peut éventuellement casser » par rapport à la méthode traditionnelle « tester chaque chemin d'exécution ». Cela conduit les développeurs à développer moins de tests que les méthodes classiques, mais ce n'est pas vraiment un problème, plutôt un rappel des faits, car les méthodes classiques ont rarement été suivies de manière suffisamment méthodique pour que tous les chemins d'exécution aient été testés en profondeur. La programmation extrême reconnaît simplement que les tests sont rarement exhaustifs (parce qu'ils sont souvent trop coûteux et trop longs pour être économiquement viables) et fournit des conseils sur la façon de concentrer efficacement les ressources limitées.

Fondamentalement, le code de test est considéré comme un artefact de projet de première classe dans la mesure où il est maintenu à la même qualité que le code d'implémentation, avec toute duplication supprimée. Les développeurs publient le code de test unitaire dans le référentiel de code en conjonction avec le code qu'il teste. Les tests unitaires approfondis de la programmation extrême permettent les avantages mentionnés ci-dessus, tels qu'un développement et une refactorisation de code plus simples et plus fiables , une intégration de code simplifiée, une documentation précise et des conceptions plus modulaires. Ces tests unitaires sont également exécutés en permanence comme une forme de test de régression .

Les tests unitaires sont également essentiels au concept de conception émergente . Comme la conception émergente dépend fortement de la refactorisation, les tests unitaires en font partie intégrante.

Cadres de tests unitaires

Les frameworks de tests unitaires sont le plus souvent des produits tiers qui ne sont pas distribués dans le cadre de la suite de compilateurs. Ils aident à simplifier le processus de test unitaire, ayant été développés pour une grande variété de langages .

Il est généralement possible d'effectuer des tests unitaires sans la prise en charge d'un framework spécifique en écrivant du code client qui exerce les unités testées et utilise des assertions , la gestion des exceptions ou d'autres mécanismes de flux de contrôle pour signaler l'échec. Les tests unitaires sans cadre sont précieux dans la mesure où il existe une barrière à l'entrée pour l'adoption des tests unitaires ; avoir peu de tests unitaires n'est guère mieux que n'en avoir aucun, alors qu'une fois un framework en place, l'ajout de tests unitaires devient relativement facile. Dans certains frameworks, de nombreuses fonctionnalités avancées de test unitaire sont manquantes ou doivent être codées manuellement.

Prise en charge des tests unitaires au niveau de la langue

Certains langages de programmation prennent directement en charge les tests unitaires. Leur grammaire permet la déclaration directe de tests unitaires sans importer de bibliothèque (qu'elle soit tierce ou standard). De plus, les conditions booléennes des tests unitaires peuvent être exprimées dans la même syntaxe que les expressions booléennes utilisées dans le code de test non unitaire, telles que ce qui est utilisé pour ifet les whileinstructions.

Les langages avec prise en charge intégrée des tests unitaires incluent :

Certains langages sans prise en charge intégrée des tests unitaires ont de très bonnes bibliothèques/frameworks de tests unitaires. Ces langues comprennent :

Voir également

Les références

Liens externes