Médaille
N°1 pour apprendre & réviser du collège au lycée.
Vérifications

Déjà plus de

1 million

d'inscrits !

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

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

Vérifier un programme nécessite de multiples tests pour en contrôler les différentes parties.

bannière definition

Définition

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

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 :

def indice_masse_corporelle(masse, taille) :

"""

Calcule l’indice de masse corporelle.

Indice arrondi à un chiffre après la virgule.

Masse exprimée en kilogrammes.

Taille exprimée en mètres."""

imc = masse / taille ** 2

return round(imc, 1)

À titre d’exemple, nous pouvons afficher le résultat de l’appel de notre fonction pour une personne pesant 70kg70 \text{kg} et mesurant 1,75m1,75 \text{m}.

print(indice_masse_corporelle(70, 1.75) :

# affiche 22.9

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 ZeroDivisionError 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.

if masse == 0 :

return ‘Impossible de calculer un IMC avec une taille nulle’

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

Nous allons recourir au mot-clé assert 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 :

assert taille != 0

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 00 en argument pour le paramètre de taille, une AssertionError survient et le programme s’arrête.

Traceback (most recent call last) :

File "imc.py", line 21, in <module>

print(indice_masse_corporelle(70, 0))

File "imc.py", line 15, in indice_masse_corporelle

assert taille != 0

AssertionError

Il est possible de faire afficher un message d’erreur personnalisé en le faisant figurer après l’expression évaluée.

assert taille != 0, ‘la taille ne peut être nulle’

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.

Traceback (most recent call last) :

File "imc.py", line 21, in <module>

print(indice_masse_corporelle(70, 0))

File "imc.py", line 15, in indice_masse_corporelle

assert taille != 0, ‘la taille ne peut être nulle’

AssertionError: la taille ne peut être nulle

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 assert 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

Le mot-clé raise 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 ValueError.

if taille == 0 :

raise ValueError

  • Comme pour asser nous pouvons préciser un message d’explication qui s’affiche lorsque l’exception est levée par raise.

raise ValueError(‘la taille ne peut être nulle’)

Si nous testons notre fonction en l’appelant avec une taille égale à zéro, nous obtenons une ValueError 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 :

if not isinstance(masse, (int, float)) :

raise TypeError(‘la masse doit être un nombre’)

if not isinstance(taille, (int, float)) :

raise TypeError(‘la taille doit être un nombre’)

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

Nous allons programmer nos tests dans un fichier séparé qui aura accès à notre fonction pour l’évaluer. Notre fonction indice_masse_corporelle() 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 indice_masse_corporelle() pour la rendre accessible.

from imc import indice_masse_corporelle

On peut ensuite aisément créer des vérifications simples pour des valeurs usuelles en ajoutant les lignes suivantes au programme test_imc :

# test avec des valeurs simples

assert indice_masse_corporelle(1, 1) == 1.0

assert indice_masse_corporelle(70, 1.75) == 22.9

assert indice_masse_corporelle(64, 1.60) == 25.0

Nous exécutons ensuite le programme test_imc depuis la ligne de commande.

python test_imc

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 :

print(‘Tous les tests ont réussi’)

  • Nous aurons ainsi la certitude que notre programme s’est correctement exécuté jusqu’à la fin.
bannière à retenir

À retenir

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.

bannière exemple

Exemple

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 95kg95 \text{kg} et mesurant 1,81m1,81 \text{m} a un IMC de 29,029,0 kgm2\text{kg}\cdot\text{m}^{-2} ;
  • une personne pesant 48kg48 \text{kg} et mesurant 1,69m1,69 \text{m} a un IMC 16,8\approx16,8 kgm2\text{kg}\cdot\text{m}^{-2} ;
  • une personne pesant 61kg61 \text{kg} et mesurant 1,57m1,57 \text{m} a un IMC de 24,7\approx24,7 kgm2\text{kg}\cdot\text{m}^{-2} ;
  • une personne pesant 140kg140 \text{kg} et mesurant 2,04m2,04 \text{m} a un IMC 33,6\approx33,6 kgm2\text{kg}\cdot\text{m}^{-2}.

Nous ajoutons les tests correspondants à notre programme de test :

assert indice_masse_corporelle(95, 1.81) == 29.0

assert indice_masse_corporelle(48, 1.69) == 16.8

assert indice_masse_corporelle(61, 1.57) == 24.7

assert indice_masse_corporelle(140, 2.04) == 33.6

  • 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.

bannière attention

Attention

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

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

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 indice_masse_corporelle(), 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 native pow(). Cette fonction prend comme premier argument le nombre et comme second argument la puissance à lui appliquer.

  • Notre ligne de calcul initiale :

imc = masse / taille ** 2

  • Est réécrite de la manière suivante :

imc = masse / pow(taille, 2)

  • Le lancement du programme de test à l’issue de cette modification nous confirme que tous nos tests continuent de passer.

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.

bannière attention

Attention

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

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.