630 likes | 784 Views
Pratique des tests logiciels. Introduction. Activité aussi vieille que le développement Souvent négligée et peu formalisée Considérée (à tort) comme moins « noble » que le développement Coût souvent > 50% du coût total d’un logiciel Très peu enseignée. Idées fausses sur les tests (1/2).
E N D
Introduction • Activité aussi vieille que le développement • Souvent négligée et peu formalisée • Considérée (à tort) comme moins « noble » que le développement • Coût souvent > 50% du coût total d’un logiciel • Très peu enseignée
Idées fausses sur les tests (1/2) • Il faut éliminer tous les défauts • Doit-on tester le cas où l’automobile roule à 500 km/h ? • L’amélioration de la fiabilité est proportionnelle au nombre de défauts corrigés • Il reste un défaut dans une fonction toujours utilisée … • Je programme sans erreur, ce n’est pas la peine de tester …
Idées fausses sur les tests (2/2) • L’amélioration de la fiabilité est proportionnelle au taux de couverture du code par les tests • Évaluer la qualité d’un logiciel, c’est estimer le nombre de défauts résiduels • Croire que c’est simple et facile • Croire que cela n’exige ni expérience, ni savoir-faire, ni méthodes
Juste un exemple 232 + 232 Faire tous les tests 0,5 milliard d’années ! (pour toutes les valeurs) En phase de test, nous sommes toujours face à l’explosion combinatoire
Les différentes sortes de tests • Tests unitaires • On teste chaque fonction de chaque module (tests « boîte blanche » sur les instructions) • Tests d’intégration • On teste les interfaces entre modules (tests « boîte blanche » sur les interfaces, tests « boîte noire » sur les instructions) • Tests de validation • On teste les fonctionnalités du logiciel (tests « boîte noire »)
Graphe de contrôle d’un programme • Tout programme peut se représenter sous forme de graphe où : • Les nœuds sont les instructions • Les arcs sont les branchements • Exemple : Un if-then-else
Les types de nœuds du graphe • Il y a trois types de nœuds : • Les nœuds « fonction » (toute instruction est une fonction) matérialisés par : • Les nœuds « prédicatifs » (représentant une condition) matérialisés par : • Les nœuds « de regroupement » (qui sont vides) matérialisés par : f c
Programmation structurée (1/3) • Si l’on examine tous les graphes de 1 à 4 nœuds (il y en a 15), seuls 7 sont pertinents car les autres n’ont pas de nœuds fonction (il n’y a donc pas d’instruction !) • Ces 7 graphes sont à l’origine de la programmation structurée
Programmation structurée (2/3) • En effet, on a : • L’instruction : a = b; par exemple • La séquence : a = b; c = d; • Remarque : correspond à la composition de fonctions • Les alternatives : • If-then • If-then-else • Les itératives : • While-do • Do…while (ou repeat…until) • Do-while-do (test de « milieu » de boucle)
La programmation structurée (3/3) • On remarque que : • Il n’y a pas le « case » (switch) • C’est un confort syntaxique pour exprimer des cascades de if-then-else imbriqués • Il n’y a pas de boucle « for » • La boucle «for » n’est pas une vraie boucle
Programme structuré • Un programme structuré est donc : • Un programme qui ne possède qu’un point d’entrée et un point de sortie • Constitué à partir des 7 formes de bases (appelées structures premières) • Tel que pour chaque nœud il existe un chemin reliant l’entrée à la sortie
Nombre cyclomatique(1/4) • À partir du graphe de programme on peut calculer le nombre cyclomatique (Mc Cabe-1976) V(g) tel que : • V(g) = e – n + 2 (e : edge, n : node) • Le nombre cyclomatique représente : • Le nombre de chemins linéairement indépendants dans un graphe fortement connexe • Le nombre de décisions + 1 prises dans un programme (i.e. le nombre de nœuds prédicatifs + 1)
Nombre cyclomatique(2/4) • Un graphe est fortement connexe si : • ni et nj, deux nœuds du graphe, un chemin reliant ni et nj • On remarque qu’un programme bien structuré n’a pas un graphe fortement connexe car il n’y a pas de chemin reliant la sortie à l’entrée ! (on ajoute donc cet arc fictif) • Le nombre de chemins linéairement indépendants correspond au nombre de régions du plan délimitées par le graphe quand les arcs ne se recoupent pas.
Nombre cyclomatique(3/4) • Exemple V(G) = 3 (donc 2 décisions)
Nombre cyclomatique(4/4) • Quelle valeur maximum ? • On considère généralement que le nombre cyclomatique doit être inférieur à 10 dans chaque sous-programme • Remarque importante • Le nombre cyclomatique ne mesure que la complexité structurelle statique
Exercice 1 // Radiation d'un abonne void Bibliotheque::RadierAbonne(int code_ab) { system("cls"); POSITION pos=la_liste_abonne.GetHeadPosition(); while (pos) { if (code_ab==la_liste_abonne.GetAt(pos).ObtenirCode()) { if (la_liste_abonne.GetAt(pos).ObtenirDateRadiation()==0) { if (la_liste_abonne.GetAt(pos).Nb_Exemp_Emprunte()==0) { la_liste_abonne.GetAt(pos).RadierAbonne(); printf("Nouveaux renseignements sur l'abonne : \n\n"); la_liste_abonne.GetAt(pos).AfficherAbonne(); printf("\n\n"); printf("\t\tAppuyez sur ENTREE pour revenir au menu"); fflush(stdin); getchar(); system("cls"); return; } else { printf("Cet abonne doit rendre des exemplaires\n\n\n"); return;} } else {printf("Cet abonne est deja radie \n\n");} return; } else {(void)la_liste_abonne.GetNext(pos);} } printf("Cet abonne n'existe pas \n\n\n"); } Faire le graphe de contrôle et calculer le nombre cyclomatique de cette fonction C++
Exercice 2 • Quel est l’ensemble minimum de tests à élaborer pour tester unitairement la fonction RadierAbonne ? • On suppose évidemment que toutes les fonctions appelées ont déjà été testées unitairement • La fonction GetNext met à jour son argument (pos)
Les effets néfastes d’une programmation non structurée Le graphe de gauche est réductible car il n’est constitué que de formes de base de la programmation structurée (ev(g)=1). Celui de droite n’est pas un programme structuré (le goto au milieu). Il n’est plus réductible ! (ev(g) = 12) Ev : complexité effective (en réduisant à 1 nœud les constructions de la programmation structurée)
Complexité et nombre de chemins (1/3) V(G) = 3 Nbre de chemins = 3 V(G) = 4 Nbre de chemins = 3n !
Complexité et nombre de chemins (2/3) Et s’il y a des dépendances fonctionnelles entre blocs … on devrait effectivement tester tous les chemins ! x = 0 y = 5/x
Complexité et nombre de chemins (3/3) • Quelques remarques • Ajouter un « if-then-else » dans une fonction multiplie par 2 le nombre de chemins • Les tests ne détectent pas l’absence de défauts • On peut couvrir tous les arcs du programme et ne pas détecter tous les défauts (voir le transparent suivant)
Défauts et couverture C1 Couverture C1 à 100% mais sans jamais exécuter le chemin de droite Ce genre de problème se répète entre Cn et Cn+1, n …
Les remèdes (1/2) • Au niveau du développement • prendre des précautions : • Tester systématiquement les divisions par zéro • Tester systématiquement le retour des appels « systèmes » • (malloc/new retournant NULL par exemple en C/C++ !) • Instrumenter le code (au moins pour le mode « debug ») • En C/C++ : utiliser la macro « assert », par exemple :double racineCarree (double x){double resultat; assert(x>=0); //algo assert(resultat * resultat==x); //Pas si simple à cause des problèmes d’arrondis ! return(resultat);} • Ajouter des niveaux de trace (les débogueurs ne suffisent pas !)
Les remèdes (2/2) • Au niveau des tests (unitaires) • Partitionner l’ensemble des valeurs d’entrée pour chaque fonction • Éliminer les cas impossibles(non pas du point de vue utilisateur, car il peut toujours faire des erreurs, mais du point de vue logique) • En déduire le jeux de tests pour chaque fonction • Si la combinatoire est trop grande alors générer les cas automatiquement (ce qui requiert en général le développement d’un outil sauf si l’on dispose d’un « macro-générateur » très général : m4 de gnu par exemple)
Exemple de partitionnement • Soit une fonction f(int i) définie dans l’intervalle fermé [1..99]. Combien de tests doit-on faire ? (en supposant qu’il n’y a pas de cas particulier dans l’intervalle considéré) • On va tester pour : • f(0) • f(1) • f(k), k [2..98] (1 seul test) • f(99) • f(100) Soit 5 tests
Testabilité (1/2) • Facteurs de bonne testabilitéIls relèvent en général de la qualité de la conception : • Architecture simple et modulaire (forte cohésion d'un composant) • Abstraction à travers les interfaces (faible couplage des composants) • Politique bien définie de traitement des erreurs
Testabilité (2/2) • Facteurs de mauvaise testabilité • Très fortes contraintes de temps et d'espace • Forte intégration des traitements • Contraintes et attributs non classés • Architecture construite par addition de fonctions • Longue chaîne de traitement
Pilotes et souches (1/4) • Comme on le voit sur le schéma précédent, un module n'est pas un programme isolé. Il sera souvent nécessaire de mettre en place (simuler) un environnement pour tester unitairement un composant. • Ceci représente une charge supplémentaire de développement, qui doit être planifiée, car l'environnement mis en place pour chacun des composants à tester unitairement ne fait pas partie de la livraison.
Pilotes et souches (2/4) • Ceci se traduira par le schéma suivant : Jeux d’essai Résultats Pilote Interface Module à tester Souche Souche Souche
Pilotes et souches (3/4) • Le rôle du pilote est de : • Lire les données de test • Les passer au module à tester • Afficher/Imprimer les résultats • Les souches simuleront la logique de comportement. En général elles se contenteront de retourner une valeur fixe conforme aux paramètres passés.
Pilotes et souches (4/4) Plus on aura respecté les principes de forte cohésion et de faible couplage, en conception, plus l'écriture des pilotes et des souches devrait être simplifié.
Tests d'intégration • Les modules ayant été testés unitairement, pourquoi faire des test d'intégration ? • Afin de tester les interfaces réelles entre modules et ainsi valider la conception préliminaire du logiciel.
Les stratégies d’intégration • Le big-bang (tous les modules ensembles) • Par lots fonctionnels (par sous-ensembles de modules) • Incrémental (module par module) : • Démarche descendante (les pilotes d'abord) • Démarche ascendante (les souches d'abord) • Démarche mixte
La stratégie du big-bang • Tous les composants sont assemblés en même temps et le logiciel est testé dans son intégralité. Le chaos est garanti • Les erreurs sont très difficiles à localiser • Les tests ne peuvent être préparés à l'avance • Difficulté de planification • Risque d'entrer dans une boucle sans fin de correction/apparition d'erreurs
L'intégration par lots fonctionnels • Cette stratégie est très liée à l'architecture fonctionnelle du logiciel • Les lots doivent être très cohérents et avec peu de dépendances entre eux • Le développement peut être progressif et planifié
La stratégie incrémentale • C'est l'antithèse du big-bang • Le logiciel est construit et testé par petits morceaux. Les erreurs sont donc plus faciles à localiser et à corriger • Les interfaces sont testées plus complètement et précisément • Une approche systématique des tests peut être employée
La stratégie incrémentale descendante (1/2) • Regrouper et intégrer d'abord les composants de plus haut niveau et simuler les composants de plus bas niveau • Avantages : • Les défauts de structure sont détectés très tôt • L'architecture générale du logiciel est mise en évidence rapidement • Les mêmes jeux de tests peuvent être réutilisés sur plusieurs étapes d'intégration et même en validation (pour certains) • La charge de travail est mieux répartie • Moins de pilotes à mettre en œuvre
La stratégie incrémentale descendante (2/2) • Inconvénients : • La logique des souches doit être élaborée. Elles doivent retourner au composant à tester des valeurs qui correspondent aux différents contextes d'appels • Les composants de bas niveau ne sont testés qu'à la fin alors que leurs interfaces peuvent être délicates (interfaces avec le matériel par exemple) • Tests incomplets si la logique des souches est difficile à simuler
La stratégie incrémentale ascendante (1/2) • Regrouper et intégrer les composants de plus bas niveau (les souches) d'abord • Les pilotes sont simulés • Avantages : • Facilité de localisation des défauts du composant intégré • Les données de tests sont plus faciles à définir
La stratégie incrémentale ascendante (2/2) • Inconvénients : • Il est plus difficile de déterminer les résultats des tests (pas de composant reflétant les fonctionnalités de haut niveau) • Les jeux de tests ne sont réutilisables ni pour les étapes suivantes d'intégration ni en validation car trop spécifiques • Les composants les mieux testés sont les composants de bas niveaux qui ne sont pas toujours les plus complexes
La stratégie incrémentale mixte (1/2) • Les démarches descendantes et ascendantes sont combinées en fonction des critères suivants : • Permettre de simplifier la mise en œuvre des tests • Intégrer les composants réutilisables en premier (du moins très tôt) • Les composants qui exigent l'utilisation de la machine cible sont intégrés dans les dernières étapes • Les composants critiques (haut niveau de contrôle, criticité des performances, …) doivent être intégrés le plus tôt possible
La stratégie incrémentale mixte (2/2) • Il s'agit de minimiser les inconvénients tout en maximisant les avantages. • Ceci exige une forte expérience du chef de projet et une forte compétence des équipes.
Critères de choix de la stratégie (1/3) • Préparation des tests : • Optimiser le nombre de tests • Faciliter la réalisation des tests • Optimiser le nombre de pilotes et souches • Mise en œuvre des tests : • Diminuer leur nombre et leur complexité • Faciliter la localisation des défauts • Minimiser l’utilisation de la machine cible (car elle peut être lourde à mettre en œuvre)
Critères de choix de la stratégie (2/3) • Aspects gestion de projet : • Minimiser la taille des équipes et diminuer la complexité organisationnelle • Faciliter le lissage de la charge, des affectations • Intégrer dès que possible (minimisation des risques) : • les modules devant être fortement testés • les modules à forte probabilité de défauts • les modules à fonctionnalités les plus critiques (vitales et centrales pour le système)
Critères de choix de la stratégie (3/3) • Le plan d'intégration, élaboré durant la phase de conception préliminaire, doit être suivi : • Il définit la stratégie dominante d'intégration et l'ordre dans lequel les composants ou éléments doivent être intégrés. • Il conditionne donc les priorités de développement et de tests unitaires sur chacun des constituants.
Le problème spécifique des IHM (1/7) • Généralités • Élaboration des spécifications de l’IHM
Le problème spécifique des IHM (2/7) • Recommandations de développement • prendre en compte les métiers des utilisateurs (profils, processus métiers) • Élaborer des cas d’utilisation très en amont du cycle de vie • Associer étroitement les futurs utilisateurs à la validation de la maquette et/ou du prototype • Choisir un outil de construction d’IHM qui utilise, si possible, le même langage de programmation que le reste de l’application (sinon, très souvent, de sérieux problèmes d’intégration surviennent)