Vérifications
Introduction :
L’écriture du code informatique d’un programme n’est qu’une partie du développement logiciel. Avant d’être utilisable, le code doit être testé pour vérifier que le programme produit exactement les résultats attendus et qu’il ne comporte pas d’erreurs. La mise au point de programmes passe donc par la mise en œuvre de différents tests logiciels.
Après avoir illustré le principe et l’utilité des tests avec un code d’illustration, nous étudierons des implémentations de tests élémentaires. Nous aborderons également le développement informatique piloté par les tests.
Principes et utilité des tests
Principes et utilité des tests
Tout code informatique peut comporter des erreurs, générer des dysfonctionnements ou ne pas produire le résultat attendu par rapport aux spécifications logicielles.
L’état critique de certains algorithmes informatiques, rend d’autant plus nécessaire le contrôle de ceux-ci. On doit s’assurer qu’ils fonctionnent correctement, et aussi qu’ils ne comportent pas de défaut pouvant entrainer des désaccords de traitement.
Test unitaires
Test unitaires
Vérifier un programme nécessite de multiples tests pour en contrôler les différentes parties.
Test unitaire :
Un test unitaire est un test qui évalue une portion réduite de code. Ce test s’assure que le code produit le résultat attendu.
Ces tests unitaires peuvent être mis en œuvre de différentes manières, avec ou sans l’aide d’outils spécialisés.
Implémentation basique d’un test
Implémentation basique d’un test
Dans la démarche la plus élémentaire, on introduit des tests conditionnels dans le code pour tenter de traiter certains problèmes.
Nous allons l’expérimenter à partir d’un code de quelques lignes, celui d’une fonction initialement dépourvue de tout test. Notre fonction calcule l’indice de masse corporelle d’une personne à partir de son poids (ou plus exactement de sa masse) et de sa taille. Cette fonction retourne la valeur de cet indice avec un arrondi à un chiffre après la virgule.
Notre code de base est le suivant :
À titre d’exemple, nous pouvons afficher le résultat de l’appel de notre fonction pour une personne pesant $70\ \text{kg}$ et mesurant $1,75\ \text{m}$.
Vous aurez remarqué l’absence de test dans notre code. Notre fonction fournit les résultats attendus tant qu’on lui transmet des valeurs correctes. Elle n’est en revanche pas à l’abri de dysfonctionnements, notamment si l’utilisateur de la fonction introduit des données insensées.
Ainsi l’appel de notre fonction avec une taille égale à zéro déclenche une erreur
quand Python tente de diviser la masse par zéro (préalablement élevée au carré).On peut y remédier très simplement en introduisant un test conditionnel préalable au calcul.
Dans cet exemple, la fonction renvoie le message « Impossible de calculer un IMC avec une taille nulle » au lieu de renvoyer l’IMC qu’elle n’aurait pas sû calculer.
- Toutefois cette solution n’est pas idéale. Le message d’erreur sera accessible à l’utilisateur sur le même plan que les affichages du programme dans son fonctionnement normal.
Si le résultat de notre fonction est ensuite passé à une autre fonction, par exemple pour préciser, à partir de l’indice, si la personne est en poids idéal, en surpoids ou en déficit de poids, cette autre fonction s’attend à recevoir une valeur numérique, et pas le texte d’un message d’erreur. On pourrait vite se retrouver avec des problèmes en cascade et des erreurs dont les conséquences pourraient apparaitre loin de leur origine.
Pour résoudre cet aspect du problème, il est possible de faire en sorte que l’erreur soit gérée et générée au niveau où elle se produit.
Implémentation d’un test avec une assertion
Implémentation d’un test avec une assertion
- Nous allons recourir au mot-cléqui permet de vérifier si une condition est vraie, et de générer une erreur si la condition n’est pas remplie.
Nous remplaçons donc notre précédent test conditionnel par la ligne suivante :
Nous pouvons vérifier que le comportement de notre fonction est inchangé tant que la taille est non nulle. En revanche, si nous introduisons la valeur $0$ en argument pour le paramètre de taille, une
survient et le programme s’arrête.Il est possible de faire afficher un message d’erreur personnalisé en le faisant figurer après l’expression évaluée.
Si nous effectuons un nouvel appel de fonction avec une valeur de taille nulle, le message d'erreur mentionne désormais la précision apportée.
Il est important de noter que le mécanisme d’assertion ne se déclenche pas si le programme n’est pas exécuté en mode debug car l’instruction
est alors ignorée. Il faut donc privilégier l’assertion à des fins de mise au point d’un programme uniquement.Implémentation d’un test avec levée d’exception
Implémentation d’un test avec levée d’exception
- Le mot-clénous permet de lever une exception, c’est-à-dire de déclencher volontairement l’interruption du programme et d’afficher le type d’erreur souhaité.
Dans notre cas, il s’agit d’une erreur liée à la valeur fournie par l’utilisateur de notre fonction. Nous indiquerons donc une
.
- Comme pournous pouvons préciser un message d’explication qui s’affiche lorsque l’exception est levée par .
Si nous testons notre fonction en l’appelant avec une taille égale à zéro, nous obtenons une
qui affiche notre message explicatif.Nous pouvons également introduire un contrôle au niveau du type de valeurs fournies en arguments, pour la masse et la taille. Notre fonction, effectuant des calculs numériques, doit recevoir des nombres, qui ne peuvent qu’être entiers ou décimaux.
- Nous ajoutons donc les tests correspondants :
Il serait souhaitable de tester que notre code livre un calcul correct, pour différentes valeurs admissibles de taille et de masse. Nous allons les implémenter en dissociant ces tests du code de la fonction elle-même.
Tests unitaires dissociés du code principal
Tests unitaires dissociés du code principal
Nous allons programmer nos tests dans un fichier séparé qui aura accès à notre fonction pour l’évaluer. Notre fonction
est enregistrée dans un fichier nommé imc.py qui peut par ailleurs comporter d’autres fonctions et un code les mettant en œuvre.Nous créons parallèlement un autre programme, destiné à effectuer différents tests unitaires que nous regrouperons dans un fichier nommé test_imc.py situé dans le même répertoire que le fichier imc.py. Notre programme de test commence par importer notre fonction
pour la rendre accessible.On peut ensuite aisément créer des vérifications simples pour des valeurs usuelles en ajoutant les lignes suivantes au programme test_imc :
Nous exécutons ensuite le programme test_imc depuis la ligne de commande.
Notre programme ne génère pas d’erreur mais il n’affiche rien, ce qui n’est pas très parlant. Ajoutons en fin de programme un affichage :
- Nous aurons ainsi la certitude que notre programme s’est correctement exécuté jusqu’à la fin.
Les tests doivent être aussi indépendants que possible du code. Il en va de même pour les calculs de valeurs arbitraires qui figurent dans nos tests. Si nous avons fait une erreur de raisonnement ou de calcul, nos tests peuvent être erronés. C’est pourquoi dans certains projets informatiques les tests sont écrits par une autre personne que l’auteur du code.
Dans le même esprit nous allons ajouter des tests basés sur les calculs effectués par des tiers, en prenant les valeurs calculées à titre d’exemple sur la fiche Wikipedia de l’indice de masse corporelle (source : https://fr.wikipedia.org/wiki/Indicedemasse_corporelle). C’est une source externe indépendante de notre code, ce qui nous permet de contrôler si notre implémentation du calcul de cet indice génère bien le résultat attendu.
Nous trouvons sur la page Wikipedia les informations suivantes :
- une personne pesant $95 \text{kg}$ et mesurant $1,81 \text{m}$ a un IMC de $29,0$ $\text{kg}\cdot\text{m}^{-2}$ ;
- une personne pesant $48 \text{kg}$ et mesurant $1,69 \text{m}$ a un IMC $\approx16,8$ $\text{kg}\cdot\text{m}^{-2}$ ;
- une personne pesant $61 \text{kg}$ et mesurant $1,57 \text{m}$ a un IMC de $\approx24,7$ $\text{kg}\cdot\text{m}^{-2}$ ;
- une personne pesant $140 \text{kg}$ et mesurant $2,04 \text{m}$ a un IMC $\approx33,6$ $\text{kg}\cdot\text{m}^{-2}$.
Nous ajoutons les tests correspondants à notre programme de test :
- Une nouvelle exécution de notre programme aboutit à l’affiche du message indiquant que tous les tests ont réussi.
Notre programme de tests constitue un ensemble de vérifications également appelé jeu de tests. Nous pouvons le lancer chaque fois que nécessaire et notamment après avoir modifié notre code, pour vérifier que nous n’avons pas introduit d’erreurs.
Toutefois ce programme n’est pas parfait : il s’arrêtera à la première erreur rencontrée et n’effectuera pas les tests suivants, ce qui impose une mise au point du code suivant la chronologie des tests et empêche de constater d’éventuelles erreurs multiples.
Il existe des bibliothèques spécialisées pour les tests qui permettent de s’affranchir de ce problème : ces bibliothèques sont capables de vérifier des jeux de tests complets sans s’arrêter à la première erreur.
Tests et démarche de développement
Tests et démarche de développement
Outre les tests unitaires, le développeur doit contrôler qu'il n'a pas altéré tout code existant dans lequel il a effectué des modifications. Par ailleurs, il importe qu'il veille à ce que l'ensemble des test unitaires qu'il a conçu et appliqué, couvre bien l'ensemble du code sur lequel il est intervenu. On va voir qu'une méthode de développement spécifique, permet de faciliter ce travail.
Non régression
Non régression
L’un des intérêts des tests unitaires est de nous protéger contre des régressions, c’est-à-dire l’introduction involontaire d’erreurs dans un code qui fonctionnait jusqu’à présent. Cela peut notamment se produire lors de la réécriture du code, parfois appelée réusinage (deux traductions possibles de l’anglais refactoring).
Dans le cas de notre fonction
, le calcul du carré de la taille est effectué avec la notation . Nous choisissons de réusiner le code de notre fonction en remplaçant cette notation d’exposant par la fonction . Cette fonction prend comme premier argument le nombre et comme second argument la puissance à lui appliquer.- Notre ligne de calcul initiale :
- Est réécrite de la manière suivante :
- Le lancement du programme de test à l’issue de cette modification nous confirme que tous nos tests continuent de passer.
Couverture des tests
Couverture des tests
Les tests prévus peuvent aussi ne pas couvrir tous les cas de figure. Dans notre cas nous avons vérifié que les valeurs passées, étaient bien numériques pour les deux paramètres et non nulle pour la taille, mais nous n’avons pas vérifié si les valeurs sont humainement insensées avec des caractères physiques impossibles comme une taille de plusieurs mètres ou des valeurs extrêmes de masse.
Malgré les précautions et contrôles apportés par les tests, des défauts de conception et des erreurs logiques présents dans le code peuvent passer inaperçus. En effet dans l’approche classique de développement, les tests sont écrits après le code et en fonction du code.
Une autre approche du développement logiciel propose de renverser les choses en commençant par écrire les tests avant le code.
Développement piloté par les tests
Développement piloté par les tests
Les jeux de tests sont la base d’une méthode de programmation basée sur les tests : cette méthode consiste en un développement informatique piloté par les tests, appelée en anglais « Test Driven Development » et abrégée TDD.
Dans cette approche du développement, on ne commence pas par écrire le code du programme mais celui d’un test du code à venir. On écrit ensuite un code suffisant pour réussir le test. On écrit alors un nouveau test, puis le code correspondant, et ainsi de suite. Le développement est donc incrémental : chaque test précise le comportement désiré de notre programme. Le test commence par échouer en l’absence de code correspondant. On écrit ensuite le code minimal qui permette de réussir le test, et bien sûr de ne pas faire échouer les tests précédents.
Ce renversement par rapport à l’approche traditionnelle du développement informatique où on commence par écrire le code peut sembler déroutant, mais il présente l’avantage de disposer d’un ensemble de tests en rapport direct avec les spécifications logicielles attendues.
Conclusion :
Les jeux de tests constituent une aide précieuse pour le développement informatique, dont ils contribuent à améliorer la qualité. Ces vérifications visent à s’assurer que le code produit est dépourvu d’erreurs et qu’il est conforme aux attentes.
Cependant, il ne faut pas oublier que le succès de l’ensemble des tests n’est pas une garantie totale de correction d’un programme. Des erreurs peuvent subsister si les tests ne couvrent pas la totalité des cas de figure, si une erreur de raisonnement passe inaperçue dans le code ou est présente parmi les tests chargés d’évaluer le code.