Les tests unitaires , également appelés tests de composants ou de modules , sont une forme de test logiciel par laquelle le code source isolé est testé pour valider le comportement attendu.
Les tests unitaires décrivent les tests exécutés au niveau de l'unité pour contraster les tests au niveau de l' intégration ou du système .
Histoire
Les tests unitaires, en tant que principe de test séparé de petites parties de grands systèmes logiciels, remontent aux débuts de l'ingénierie logicielle. En juin 1956, HD Benington a présenté au symposium de la marine américaine sur les méthodes de programmation avancées pour les ordinateurs numériques le projet SAGE et son approche basée sur les spécifications où la phase de codage était suivie de « tests de paramètres » pour valider les sous-programmes des composants par rapport à leurs spécifications, suivis ensuite d'un « test d'assemblage » pour les parties assemblées.
En 1964, une approche similaire est décrite pour le logiciel du projet Mercury , où les unités individuelles développées par différents programmes subissent des « tests unitaires » avant d'être intégrées ensemble. En 1969, les méthodologies de test apparaissent plus structurées, avec des tests unitaires, des tests de composants et des tests d'intégration dans le but de valider les parties individuelles écrites séparément et leur assemblage progressif en blocs plus grands. Certaines normes publiques adoptées à la fin des années 60, telles que MIL-STD-483 et MIL-STD-490 ont contribué davantage à une large acceptation des tests unitaires dans les grands projets.
À cette époque, les tests unitaires étaient interactifs ou automatisés , utilisant soit des tests codés, soit des outils de test de capture et de relecture. En 1989, Kent Beck a décrit un cadre de test pour Smalltalk (plus tard appelé SUnit ) dans « Simple Smalltalk Testing: With Patterns ». En 1997, Kent Beck et Erich Gamma ont développé et publié JUnit , un cadre de test unitaire qui est devenu populaire auprès des développeurs Java . Google a adopté les tests automatisés vers 2005-2006.
Unité
L'unité est définie comme un comportement unique présenté par le système testé (SUT), correspondant généralement à une exigence. Bien que cela puisse impliquer qu'il s'agisse d'une fonction ou d'un module (en programmation procédurale ) ou d'une méthode ou d'une classe (en programmation orientée objet ), cela ne signifie pas que les fonctions/méthodes, les modules ou les classes correspondent toujours à des unités. Du point de vue des exigences du système, seul le périmètre du système est pertinent, ainsi seuls les points d'entrée vers des comportements du système visibles de l'extérieur définissent les unités.
Exécution
Les tests unitaires peuvent être effectués manuellement ou via une exécution de test automatisée . Les tests automatisés présentent des avantages tels que : l'exécution fréquente de tests, l'exécution de tests sans frais de personnel et des tests cohérents et reproductibles.
Les tests sont souvent effectués par le programmeur qui écrit et modifie le code testé. Les tests unitaires peuvent être considérés comme faisant partie du processus d'écriture du code.
Critères de test
Au cours du développement, un programmeur peut coder des critères ou des résultats connus pour être bons dans le test pour vérifier l'exactitude de l'unité.
Lors de l'exécution des tests, les frameworks enregistrent les tests qui échouent à un critère et les signalent dans un résumé.
Pour cela, l’approche la plus couramment utilisée est test – fonction – valeur attendue.
Cas de test
Double test
Test paramétré
Un test paramétré est un test qui accepte un ensemble de valeurs pouvant être utilisées pour permettre au test de s'exécuter avec plusieurs valeurs d'entrée différentes. Un framework de test prenant en charge les tests paramétrés prend en charge un moyen d'encoder des ensembles de paramètres et d'exécuter le test avec chaque ensemble.
L’utilisation de tests paramétrés peut réduire la duplication du code de test.
Les tests paramétrés sont pris en charge par TestNG , JUnit , XUnit et NUnit , ainsi que dans divers frameworks de test JavaScript.
Les paramètres des tests unitaires peuvent être codés manuellement ou, dans certains cas, générés automatiquement par le framework de test. Ces dernières années, un support a été ajouté pour l'écriture de tests (unitaires) plus puissants, en exploitant le concept de théories, des 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ées prédéfinis.
Agile
Parfois, dans le développement agile de logiciels, les tests unitaires sont effectués par user story et interviennent dans la seconde moitié du sprint, une fois la collecte des exigences et le développement terminés. En général, les développeurs ou d'autres membres de l'équipe de développement, tels que les consultants , écrivent des « scripts de test » étape par étape que les développeurs doivent exécuter dans l'outil. Les scripts de test sont généralement écrits pour prouver le fonctionnement efficace et technique de fonctionnalités spécifiques développées dans l'outil, par opposition aux processus métier à part entière qui seraient interfacés par l' utilisateur final , ce qui est généralement fait pendant les tests d'acceptation utilisateur . Si le script de test peut être entièrement exécuté du début à la fin sans incident, le test unitaire est considéré comme « réussi », sinon des erreurs sont notées et l'user story est renvoyée au développement dans un état « en cours ». Les user stories qui passent avec succès les tests unitaires passent aux dernières étapes du sprint : revue du code, revue par les pairs, puis enfin une session de « présentation » de l'outil développé aux parties prenantes.
Développement piloté par les tests
Dans le développement piloté par les tests (TDD), les tests unitaires sont écrits pendant que le code de production est écrit. En commençant par le code fonctionnel, le développeur ajoute du code de test pour un comportement requis, puis ajoute juste assez de code pour que le test réussisse, puis refactorise le code (y compris le code de test) comme cela a du sens, puis répète en ajoutant un autre test.
Valeur
Les tests unitaires visent à garantir que les unités répondent à leur conception et se comportent comme prévu.
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.
L’un des objectifs des tests unitaires est d’isoler chaque partie du programme et de montrer que les parties individuelles sont correctes. contrat écrit strict que le morceau de code doit satisfaire.
Détection précoce des problèmes dans le cycle de développement
Les tests unitaires détectent les problèmes dès le début du cycle de développement . Cela comprend à la fois les bugs dans l'implémentation du programmeur et les défauts ou parties manquantes de la spécification de l'unité. Le processus de rédaction d'un ensemble complet de tests oblige l'auteur à réfléchir aux entrées, aux sorties et aux conditions d'erreur, et donc à définir plus précisément le comportement souhaité de l'unité.
Coût réduit
Le coût de la détection d'un bug avant le début du codage ou lors de la première écriture du code est considérablement inférieur au coût de détection, d'identification et de correction du bug ultérieurement. Les bugs 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 à structurer les fonctions et les objets de manière plus efficace.
Des sorties plus fréquentes
Les tests unitaires permettent des versions plus fréquentes dans le développement de logiciels. En testant des composants individuels de manière isolée, les développeurs peuvent rapidement identifier et résoudre les problèmes, ce qui permet d'accélérer les cycles d'itération et de publication.
Permet la refactorisation du code
Les tests unitaires permettent au programmeur de refactoriser le code ou de mettre à niveau les bibliothèques système ultérieurement 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 identifié rapidement.
Détecte les modifications susceptibles de rompre un contrat de conception
Les tests unitaires détectent les changements susceptibles de rompre un contrat de conception .
Réduire l’incertitude
Les tests unitaires peuvent réduire l'incertitude dans les unités elles-mêmes et peuvent être utilisés dans une approche de test ascendante . 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.
Documentation du comportement du système
Certains programmeurs affirment que les tests unitaires fournissent une forme de documentation du code. Les développeurs souhaitant savoir quelles fonctionnalités sont fournies par une unité et comment les utiliser peuvent consulter les tests unitaires pour en comprendre le fonctionnement.
Les cas de test peuvent contenir 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 documente ces caractéristiques critiques, bien que de nombreux environnements de développement de logiciels ne s'appuient pas uniquement sur le code pour documenter le produit en cours de développement.
Dans certains processus, l'acte d'écrire des tests et le code testé, ainsi que la refactorisation associée, peuvent 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
Les tests ne détecteront pas toutes les erreurs du programme, car ils ne peuvent pas évaluer tous les chemins d'exécution, sauf dans les programmes les plus triviaux. Ce problème est un sur-ensemble du problème d'arrêt , qui est indécidable . Il en va 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, ils ne détecteront 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 domaines de test non fonctionnels tels que les performances ). Les tests unitaires doivent être effectués en conjonction 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 pas prouver une absence complète 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 largement sur des tests manuels effectués par des humains ; les tests de haut niveau ou de portée globale peuvent être difficiles à automatiser, de sorte que les tests manuels semblent souvent plus rapides et moins chers.
Les tests logiciels sont 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 l'investissement peut ne pas en valoir la peine. Certains problèmes ne peuvent pas être facilement testés du tout, par exemple ceux qui ne sont pas déterministes ou impliquent plusieurs threads . De plus, le code d'un test unitaire est aussi susceptible d'être 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 ?
Difficulté à mettre en place des tests réalistes et utiles
Un autre défi lié à l'écriture 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.
Nécessite de la discipline tout au long du processus de développement
Pour obtenir les avantages escomptés des tests unitaires, une discipline rigoureuse est nécessaire tout au long du processus de développement logiciel.
Nécessite un contrôle de version
Il est essentiel de conserver soigneusement des enregistrements non seulement des tests qui ont été effectués, mais aussi de toutes les modifications qui ont été 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 essentielle. 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.
Nécessite des révisions régulières
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 intégré dans le flux de travail de l'équipe, l'application évoluera en décalage avec la suite de tests unitaires, augmentant les faux positifs et réduisant l'efficacité de la suite de tests.
Limitations des logiciels de systèmes embarqués
Les tests unitaires des logiciels de systèmes 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 fonctionnera finalement, vous ne pouvez pas exécuter facilement un programme de test dans l'environnement de déploiement réel, comme c'est possible avec les programmes de bureau.
Limitations des tests d'intégration avec des systèmes externes
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 principale 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 des interactions de la base de données, qui ne sera probablement pas aussi complète que les interactions réelles de la base de données.
Exemples
Unité JUnit
Vous trouverez ci-dessous un exemple de suite de tests JUnit. Elle se concentre sur la Adderclasse.
classe Adder { public int add ( int a , int b ) { renvoie a + b ; } }
La suite de tests utilise des instructions d'assertion pour vérifier le résultat attendu de diverses valeurs d'entrée de la summéthode.
importer statique org.junit.Assert.assertEquals ; importer org.junit.Test ; classe publique AdderUnitTest { @Test public void sumReturnsZeroForZeroInput () { Adder adder = new Adder (); assertEquals ( 0 , adder . add ( 0 , 0 )); } @Test public void sumReturnsSumOfTwoPositiveNumbers () { Additionneur additionneur = nouvel additionneur ( ); assertEquals ( 3 , additionneur . add ( 1 , 2 )); } @Test public void sumReturnsSumOfTwoNegativeNumbers () { Additionneur additionneur = nouvel additionneur ( ); assertEquals ( - 3 , additionneur . add ( - 1 , - 2 )); } @Test public void sumReturnsSumOfLargeNumbers () { Additionneur additionneur = nouvel additionneur (); assertEquals ( 2222 , additionneur . add ( 1234 , 988 )); } }
En tant que spécifications exécutables
L'utilisation de 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 l'implémentation. Les tests ne réussiront jamais à moins que le développeur implémente une solution conforme à la conception.
Les tests unitaires ne sont pas aussi accessibles qu'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 disposent d'outils gratuits (généralement disponibles sous forme d'extensions aux IDE ). Les outils gratuits, comme ceux basés sur le framework xUnit , externalisent vers 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és . Ce cadre de tests unitaires automatisés peut être tiers, par exemple xUnit , ou 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 que le test, ainsi que d'autres tests, réussissent.
La plupart des codes d'un système sont testés 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 de « 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 une réaffirmation 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 (car ils sont souvent trop coûteux et prennent trop de temps pour être économiquement viables) et fournit des conseils sur la manière de concentrer efficacement des ressources limitées.
Le code de test est considéré comme un artefact de projet de première classe, car il est maintenu à la même qualité que le code d'implémentation, toutes les duplications étant supprimées. 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 sûrs , 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 sous forme de test de régression .
Les tests unitaires sont également essentiels au concept de conception émergente . Comme la conception émergente dépend fortement du refactoring, les tests unitaires en font partie intégrante.
Cadres de tests automatisés
Un framework de tests automatisés fournit des fonctionnalités permettant d'automatiser l'exécution des tests et peut accélérer l'écriture et l'exécution des tests. Des frameworks ont été développés pour une grande variété de langages de programmation .
En général, les frameworks sont tiers ; ils ne sont pas distribués avec un compilateur ou un environnement de développement intégré (IDE).
Les tests peuvent être écrits sans utiliser de framework pour tester le code testé à l'aide d'assertions , de gestion des exceptions et d'autres mécanismes de contrôle de flux pour vérifier le comportement et signaler les échecs. Certains notent que les tests sans framework sont utiles car il existe une barrière à l'entrée pour l'adoption d'un framework ; qu'avoir quelques tests est mieux que rien, mais une fois qu'un framework est en place, l'ajout de tests peut être plus facile.
Dans certains frameworks, des fonctionnalités de test avancées sont manquantes et doivent être codées manuellement.
Prise en charge des tests unitaires au niveau du langage
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 les instructions what is used for ifet while.
Les langages avec prise en charge intégrée des tests unitaires incluent :
Les langages avec prise en charge du framework de tests unitaires standard incluent :
Certains langages ne disposent pas de prise en charge intégrée des tests unitaires, mais disposent de bibliothèques ou de frameworks de tests unitaires établis. Ces langages incluent :
- ABAP
- C++
- C#
- Clojure
- Élixir
- Java
- JavaScript
- Objectif-C
- Perl
- PHP
- PowerShell
- R avec testthat
- Échelle
- tcl
- Visual Basic .NET
- Xojo avec XojoUnit