Techniques de tests Aziz Salah, Professeur, Département d’informatique (Hiver 2005) Guy Tremblay, Professeur, Département d’informatique (Été 2005)
Rôle et objectif des tests Le processus de test consiste à exécuter un programme dans le but de trouver les erreurs qu’il contient Observation (E.W. Dijsktra): « Les tests peuvent démontrer la présence d’erreurs, mais pas leur absence »
Processus de test Évaluation Exécution du test Débogage Cas de test Résultat du test Résultat attendu Défectuosité Correction
Que montrent les tests? Présence d’erreurs de programmation? Respect des exigences? Performance satisfaisante? Qualité acceptable?
Qui doit tester le logiciel? Le développeur Un testeur indépendant (Tests unitaires, tests d’intégration) (Tests d’intégration, tests de système)
Qui doit tester? Il est important que le développeur teste son code (tests unitaires) avant de l’intégrer au reste du système Mais: il est difficile pour un développeur de tester son propre code car il faut tester pour trouver les erreurs dans le code, pas pour montrer que le code fonctionne En d’autres mots, un test réussit à faire son travail lorsqu’il trouve une erreur, pas lorsqu’il montre que le code fonctionne correctement
Les principales phases de test Tests d’acceptation Tests de système Tests d’intégration Tests unitaires Aziz: peux-tu corriger les flèches de la figure pour qu’elles relient correctement les boîtes et supprime la dernière (celle qui sort des tests d’acceptation)?
Les principales phases de test Tests unitaires: tester les modules au fur et à mesure de leur développement Tests d’intégration: tester l’assemblage des modules et composants et leurs interfaces Tests de système: tester les fonctionnalités du système dans son ensemble Tests d’acceptation: confirmer que le système est prêt à être livré et installé
Autres types de tests Tests de (non) régression: tests déjà exécutés qu’on exécute à nouveau pour déterminer si le programme (qui vient d’être modifié) fonctionne toujours Tests alpha et beta: tests d’acceptation effectués avec des clients dans l’environnement de développement (alpha) ou dans les sites clients (beta) Autres : tests de performance, tests de stress, tests d’utilisabilité, etc.
Fait : il est impossible de faire des tests exhaustifs, qui couvrent toutes les possibilités Boucle avec 20 itérations Si l’exécution d’un cas de test (pour un chemin allant du début à la fin du programme) prend une milliseconde, alors il faudrait plusieurs milliers d’années pour tester tous les chemins possibles de ce programme
Donc: on exécute les tests de façon sélective mais judicieuse Identification des chemins indépendants Nombre limité, mais judicieusement choisi, d’exécution des boucles Partitionnement des données en classes d’équivalence Analyse des cas limites Etc.
Deux grandes méthodes de tests Tests fonctionnels, de type boîte noire Tests structurels, de type boîte blanche Méthodes Stratégies
Tests de type boîte blanche Tests structurels : basés sur la structure interne du code à tester = on connaît la structure interne du programme et on en tient compte pour développer les cas de test
Pourquoi il est important de faire des tests de type boîte blanche Fait: on risque plus facilement de se tromper (erreur de logique, de codage) en traitant un cas peu fréquent -- i.e., probabilité faible d’exécution et probabilité élevée d’erreur vont souvent de paire Fait: certains chemins qui, intuitivement, nous semblent improbables, peuvent quand même être exécutés Fait: les erreurs typographiques sont réparties au peu hasard et il est hautement probable qu’un chemin qui n’a pas été testé en contienne
Tests de type boîte blanche Cas de test Analyser la structure du module et définir les cas de test appropriés tester Composant ou module Sorties attendues pour chacun des tests
Niveaux de couverture Chemin complet dans un programme = séquence spécifique d’instructions allant du début du programme jusqu’à la fin Différents objectifs possibles de tests (différents niveaux de couverture du code) N0: Exécuter tous les chemins possibles au moins une fois (couverture exhaustive) N1: Exécuter toutes les instructions du programme au moins une fois (couverture des instructions) N2: Exécuter toutes les branches des conditions, dans chaque direction, au moins une fois (couvertures des branchements et conditions)
Niveaux de couverture Or: N0 est impossible en présence de boucles : on ne peut pas tester tous les chemins distincts dans le cas d’une boucle avec un grand nombre (possiblement variable) d’itérations (chaque façon de terminer la boucle est un chemin distinct) N1 n’est pas suffisant : si une instruction if ne possède pas de branche else, on peut avoir exécuté toutes les instructions, mais sans avoir testé ce qui arrive lorsque la condition est fausse Si le programme est bien structuré (pas de goto), alors N2 implique N1
Niveaux de couverture Objectif = définir suffisamment (mais pas trop) de cas de tests pour générer des chemins d’exécution à travers le programme qui assureront que les niveaux de couverture N1 et N2 seront atteints Stratégie possible = examiner la structure du code, via le graphe de flux de données du programme (voir plus loin), pour identifier un ensemble de chemins indépendants Note: Un chemin indépendant permet d’introduire au moins une nouvelle instruction ou une nouvelle condition dans l’ensemble des instructions et conditions qui sont couvertes
Graphe de flux de contrôle Représentation abstraite et simplifiée de la structure du programme Décrit le flux de contrôle du programme Chaque nœud représente un segment de code strictement séquentiel (sans choix ou boucle) Chaque arc dénote un lien de contrôle (possibilité d’aller d’un nœud à l’autre) Utile pour calculer la complexité cyclomatique et déterminer le nombre de cas de tests à définir pour assurer une couverture acceptable
Les graphes de flux pour diverses structures de contrôle if-then-else while do … while switch
Exemple de graphe de flux de contrôle pour un programme avec des conditions simples Région = suite d’instructions toujours strictement consécutives Conditions simples: variables booléennes ou relations entre variables (=, !=, <, <=, >, >=) Graphe de flux de contrôle Programme représenté par un organigramme
Exemple de graphe de flux de contrôle pour un programme avec des conditions composées if (a || b) then x else y x F b V x y On associe plusieurs chemins à la condition complexe « a || b »
Cas de test basés sur les chemins indépendants Suivre le processus suivant: Construire le graphe de flux de contrôle du programme. Calculer la complexité cyclomatique à partir du graphe de flux de contrôle résultant (page suivante). Identifier un ensemble chemins complets (du début à la fin du programme) indépendants. Construire des cas de test qui vont forcer l’exécution de chacun des chemins identifiés.
Complexité cyclomatique La complexité cyclomatique est une mesure de la complexité logique d’un programme Dans le contexte des tests, la complexité cyclomatique indique le nombre maximum des chemins complets indépendants, ce qui donne le nombre de cas de tests nécessaires pour assurer une bonne couverture du code (niveaux N1 et N2). Différentes façons (équivalentes) de la calculer: V(G) = #Arcs - #Noeuds + 2 = 11-9+2 = 4 V(G) = #Régions = 4 V(G) = #(Conditions)+1 = 3+1 Graphe de flux de contrôle
Exemple d’identification des chemins indépendants On doit identifier quatre chemins indépendants, par exemple : Chemin 1: 1-11 Chemin 2: 1-2,3-4,5-10-1-11 Chemin 3: 1-2,3-6-8-9-10-1-11 Chemin 4: 1-2,3-6-7-9-10-1-11
Exemple tiré de (Pressman,2001)
Graphe de flux de contrôle pour la procédure average
Un ensemble de chemins indépendants 1-2-10-11-13 1-2-10-12-13 1-2-3-10-11-13 1-2-3-4,5-8-9-2-3-… 1-2-3-4,5-6-8-9-2-3-… 1-2-3-4,5-6-7-8-9-2-3-…
Cas de test du chemin 1 Spécification des entrées Value (k) = entrée valide, avec k < i Value (i) = -999 avec 2 i 100 Spécification des sorties: Résultats attendus Une moyenne correctes des k valeurs ainsi que leur somme Note: Le chemin 1 ne peut être testé tout seul: il sera testé avec les chemins 4, 5, et 6.
Cas de test du chemin 2 Entrée Résultats attendus Value (1) = -999 average = -999; les autres champs de total restent à leur valeur initiale
Cas de test du chemin 3 Entrée Résultats attendus Essayer de traiter 101 valeurs ou plus Les première 100 valeurs devraient être valides Résultats attendus Moyenne correcte des 100 premières valeurs
Cas de test du chemin 4 Entrée Résultats attendus Value (i) = entrée valide, i < 100 Value (k) < minimum avec k<i Résultats attendus Moyenne valide
Cas de test du chemin 5 Entrée Résultats attendus Value (i) = entrée valide avec i < 100 Value (k) > maximum avec k i Résultats attendus Une moyenne correcte des n valeurs ainsi que leur somme
Cas de test du chemin 6 Entrée Résultats attendus Value (i) = entrée valide avec i< 100 Résultats attendus Une moyenne correcte des n valeurs ainsi que leur somme
Tests des boucles Boucle Simple Boucles imbriquées Les tests basés sur les chemins indépendants vont assurer de couvrir toutes les instructions et conditions. Toutefois, ce n’est pas suffisant pour assurer le bon fonctionnement d’une boucle puisqu’elle peut être exécutée un nombre variable de fois (0, 1, 2, …) Boucle Simple Boucles imbriquées
Tests de boucles simples Les différents cas à tester pour une boucle simple, où n = nombre maximum d’itérations: Ne pas exécuter la boucle (exécuter 0 fois) Exécuter la boucle une seule fois Exécuter la boucle deux fois Exécuter la boucle m fois avec 2 < m < n Exécuter la boucle n-1, n, n+1 fois
Test de boucles imbriquées ou concaténées Commencer à la boucle la plus profonde et lui appliquer le test de la boucle simple en maintenant les boucles extérieures à leurs valeurs minimales Avancer d’une boucle vers l’extérieur et lui appliquer le test de la boucle simple en maintenant les autres boucles extérieures à leurs valeurs minimales et les boucles intérieures à leurs valeurs typiques jusqu’à ce que la boucle extérieure soit testée
Tests de type boîte noire Exigences et spécifications Événements Entrées valides et invalides Sorties Appelés aussi tests fonctionnels Les tests de type boîte noire représentent une approche complémentaire de test, donc les deux approches devraient être utilisées Avantages des tests fonctionnels : leur planification et conception peut commencer tôt dans le processus de développement du logiciel, i.e., aussitôt que les spécifications sont disponibles Les types d’erreurs visées sont: Fonctionnalité incorrecte ou manquante Erreur d’interface Erreur de structure de données ou d’accès aux bases de données externes Erreur d’initialisation ou de terminaison Système
Techniques de tests de type boîte noire (tests fonctionnels) Tests des conditions limites Tests basés sur le partitionnement en classes d’équivalence Tests basés sur les pré/post-conditions
Tests des conditions limites Motivation: très souvent, les erreurs surviennent dans le traitement des cas limites (par exemple, erreur «off-by-1» dans les boucles) Donc: il est important d’identifier les cas limites, tant sur les entrées que sur les résultats, et de spécifier des cas de tests appropriés Note: tester sur les sorties signifie tenter de produire un résultat qui satisfait certaines conditions spécifiques Remarque: les tests des conditions limites sont aussi applicables pour les tests de type boîte blanche
Tests des conditions limites Valeurs numériques définies par un intervalle [a..b]: on teste avec a-1, a, a+1, b-1, b, b+1 Chaînes de caractères (avec au plus n caractères): chaîne vide, chaîne avec un seul caractère, chaîne avec n-1, n, n+1 (?) caractères Tableaux: tableau vide, avec un seul élément, avec n-1 ou n éléments Fichiers: fichier vide, avec une seule ligne, etc.
Partitionnement en classes d’équivalence Sorties invalides Entrées invalides Système Entrées valides Sorties valides
Partitionnement en classes d’équivalence Objectif = minimiser le nombre de cas de tests tout en s’assurant de couvrir tous les cas importants On suppose que des entrées équivalentes vont être traitées de façon similaire, donc on va grouper les entrées en classes d’équivalence: Le programme se comporte de manière semblable pour chaque élément d’une même classe d’équivalence parce que ces données obéissent aux mêmes conditions Ensuite, on choisit un nombre limité de cas de test dans chacune des classes d’équivalence
Exemple de partitionnement en classes d’équivalence Supposons que la donnée à l’entrée est un entier naturel qui doit contenir cinq chiffres, donc un nombre compris entre 10,000 et 99,999. Le partitionnement en classes d’équivalence identifierait alors les trois classes suivantes (trois conditions possibles pour l’entrée): N < 10000 10000 <= N <= 99999 N >= 100000 On va alors choisir des cas de test représentatif de chaque classe, par exemple, au milieu et aux frontières (cas limites) de chacune des classes: 0, 5000, 9999 10000, 50000, 99999 100000, 100001, 200000
Tests basés sur les pré/post-conditions On construit des cas de test qui vont assurer que les pré-conditions seront vérifiées de différentes façons, et aussi qu’elles ne seront pas vérifiées Si possible, on fait de même pour les post-conditions: on construit des cas de tests qui vont produire, de différentes façons, des résultats qui satisfont les post-conditions
Exemple de cas de tests basés sur les pré/post-conditions Soit la fonction suivante : float racineCarre( float x, float precision ) PRECONDITION x >= 0 && 0.0001 < precision < 0.01 x – precision <= res^2 <= x + precision
Exemple de cas de tests basés sur les pré/post-conditions Différents cas de tests qui satisfont la pré-condition, et ce de différentes façons: x = 0 ET precision = 0.0002 x = 0 ET precision = 0.005 x = 0 ET precision = 0.0099 x = 0.1 ET precision = 0.0002 x = 0.1 ET precision = 0.005 x = 0.1 ET precision = 0.0099 x = 1000.0 ET precision = 0.0002 x = 1000.0 ET precision = 0.005 x = 1000.0 ET precision = 0.0099
Références B. Beizer (1990) Software Testing Techniques, 2nd ed. Van Nostrand Reinhold Co. C. Kaner, J. Falk and H.Q. Nguyen (1999) Testing Computer Software, 2nd ed. John Wiley & Sons. I. Sommerville (2004) Software Engineering, 7th ed. Addison-Wesley. M.L. Hutcheson (2003) Software Testing Fundamentals--Methods and Metrics. John Wiley & Sons. R.S. Pressman (2001) Software Engineering: A Practitioner's Approach, 5th ed. McGraw-Hill. S. McConnell (2004) Code Complete, 2nd ed. Microsoft Press.