Assemblage testable et validation de composants

Le test de logiciel

Dans cette première section, nous présentons l’approche générale pour le test de logiciels, puis nous détaillons les travaux particuliers au test de logiciels orientés objet. Enfin, nous introduisons l’analyse de mutation, une technique particulière pour la validation et la génération de cas de test qui est utilisée dans les chapitre 3 et 4.

Le test classique

Le test de logiciel a pour but de détecter des erreurs dans un programme. Les termes de faute, erreur et défaillance ont été définis précisément dans [Laprie »95]. Une faute désigne la cause d’une erreur, une erreur est la partie du système qui est susceptible de déclencher une défaillance, et une défaillance correspond à un événement survenant lorsque le service délivré dévie de l’accomplissement de la fonction du système. Dans la suite de ce chapitre nous n’utiliserons que le terme erreur pour désigner le but du test.
Très schématiquement, le test dynamique de logiciel consiste à exécuter un programme avec un ensemble de données en entrée, et à comparer les résultats obtenus avec les résultats attendus. Si les résultats diffèrent, une erreur a été détectée, auquel cas, il faut localiser cette erreur et la corriger. Quand l’erreur est corrigée, il convient de retester le programme pour s’assurer de la correction et la non régression du programme. Enfin, pour être complète, l’activité de test implique de déterminer un « critère d’arrêt », qui spécifie quand le programme a été suffisamment testé.
A chacune de ces étapes pour le test correspond un terme particulier. Pour fixer la terminologie, et en s’inspirant de [Xanthakis »92], on considère que le programme testé est appelé programme sous test que les données de test désignent les données en entrée du programme sous test, et qu’un cas de test désigne le programme chargé d’exécuter le programme sous test avec une donnée de test particulière. Le cas de test se charge aussi de mettre le programme sous test dans un état approprié à l’exécution avec les données de test en entrée. Par exemple, pour tester le retour d’un livre dans une bibliothèque, il faut d’abord emprunter le livre. Au cours du chapitre 3, nous nous intéressons au problème de la génération automatique de données de test efficaces. L’efficacité de ces données est évaluée grâce à l’analyse de mutation qui est présentée dans la suite de ce chapitre. Après l’exécution de chaque cas de test, il faut comparer le résultat obtenu avec le résultat attendu. Ce prédicat qui permet de déterminer si le résultat est incorrect est appelé un oracle ou une fonction d’oracle. Ce prédicat peut être explicite dans les cas de test, être obtenu indirectement par des assertions ou d’autres moyens (par exemple, le model-checking) ou, dans le cas le pire, être implicite et dépendre d’un verdict humain. Il est clair que l’oracle peut être plus ou moins spécifique à la donnée de test, et pourra ne pas détecter une erreur. Par contre, on attend de l’oracle d’être correct, c’est-à-dire de ne pas marquer comme erroné un comportement correct. L’oracle est donc le reflet, plus ou moins complet, et exécutable, de la spécification.
Lorsqu’un cas de test échoue, il faut localiser la source de la défaillance dans le programme pour pouvoir corriger la faute. Cette étape de localisation est appelée la phase de diagnostic. Lorsque le résultat obtenu est conforme à l’oracle, la dernière étape du test consiste à déterminer si les cas de test sont suffisant pour garantir la qualité du logiciel. Pour cela, il faut définir un critère de test ou critère d’arrêt et vérifier si les cas de test générés vérifient ou non ce critère. Les techniques de génération visent donc souvent à vérifier un critère d’arrêt particulier (plutôt qu’à chercher à détecter des erreurs). Pour générer des cas de test en fonction d’un critère de test, il est possible de définir des objectifs de test. Par exemple, si le critère exige l’exécution de toutes les instructions du programme au cours du test, « couvrir l’instruction 11 » est un objectif de test possible. Il faut ensuite écrire un cas de test qui vérifie cet objectif. Cette notion d’objectif de test s’applique non seulement aux aspects structurels (couverture de code), mais aussi aux aspects comportementaux (observation d’un certain échange de messages [Pickin »02]). Le chapitre 5 définit un critère de test pour les logiciels OO, et s’en sert comme support pour une analyse prédictive de la testabilité visant à estimer l’effort de test.
Les techniques pour la génération de cas de test sont de deux types : le test fonctionnel et le test structurel [Beizer »90]. Si le programme sous test est considéré comme une boîte noire (on ne tient pas compte de la structure interne du programme), on parle de test fonctionnel. Dans ce cas, la génération de cas de test se fait à partir d’une spécification la plus formelle possible du comportement du programme. Cette technique a pour avantage de générer des cas de test qui seront réutilisables même si l’implantation change ; elle ne garantit cependant pas que tout le programme ait été couvert par les cas de test.
Le test structurel s’appuie sur la structure interne du programme pour générer des cas de test. Les techniques de test structurel se basent généralement sur une représentation abstraite de cette structure interne, un modèle du programme, très souvent un graphe (graphe flot de contrôle [Beizer »90], graphe flot de données [Rapps »85]). Cette représentation permet de définir des critères de couverture indépendants d’un programme particulier : couverture de tous les arcs du graphe, tous les nœuds, tout ou partie des chemins… Le test structurel a pour avantage de permettre de valider les cas de test générés, en fonction de leur taux de couverture du critère.
Le test a lieu à différents moments dans le cycle de développement du logiciel, les différentes étapes sont traditionnellement les suivantes :
 le test unitaire : une unité est la plus petite partie testable d’un programme, c’est souvent une procédure ou une classe dans les programmes à objet.
 le test d’intégration : consiste à assembler plusieurs unités et à tester les erreurs liées aux interactions entre ces unités (et éventuellement à détecter les erreurs rémanentes non détectées au niveau unitaire).
 le test système : teste le système dans sa totalité en intégrant tous les sous-groupes d’unités testés lors du test d’intégration. Il s’étend souvent à d’autres aspects tels que les performances, le test en charge…

Le test de logiciels orientés objet

Les mécanismes propres aux langages orientés objet et les méthodes d’analyse de conception et de développement associées, ont entraîné la nécessité de nouvelles techniques de test pour les logiciels fondés sur ces langages et méthodes. Une première modification a été le changement d’échelle pour le test unitaire. En effet, cette partie du test se concentrait sur les procédures dans le cadre des langages procéduraux, alors qu’elle s’intéresse à une classe dans un cadre orienté objet.
Il existe de nombreux travaux sur le test de classe, concernant les différents problèmes que sont la génération de données de test, les critères de couverture, la production d’un oracle, et l’écriture des drivers de test. En ce qui concerne les critères de couverture, [Harrold »94; Buy »00] étudient les critères flot de données et flot de contrôle pour le test d’une classe. Dans [Harrold »94], Harrold et al. proposent trois niveaux de granularité (intra-méthode, inter-méthode et intra-classe) pour l’analyse des flots de données dans une classe. Ils donnent l’algorithme pour construire le graphe et l’illustrent sur une classe. Buy et al. [Buy »00] repartent des travaux de Harrold et étendent l’approche à des techniques d’exécution symbolique et de déduction automatique, dans le but de générer automatiquement des séquences d’appels de méthodes pour tester une classe. La combinaison de ces trois techniques pour la génération de test est illustrée en détail sur un exemple. Les auteurs ont étudié, à la main, la faisabilité de l’approche sur plusieurs études de cas, et veulent maintenant développer les outils pour l’automatiser. Quant à McGregor [McGregor »01], il propose trois critères fondés sur la couverture de la machine à états associée à la classe, sur la résolution des paires de contraintes pré/post conditions, et la couverture de code.
Pour la génération des données de test plusieurs solutions ont été proposées. Dans [Buy »00], les auteurs s’intéressent à l’exécution symbolique, McGregor [McGregor »01], propose d’utiliser les contraintes les pré et post conditions exprimées en OCL pour déduire les données de test pertinents automatiquement. Dans la suite de cette thèse nous étudions deux algorithmes évolutionnistes pour la génération automatique de données de test pour un composant orienté objet ces travaux ont été publiés dans [Baudry »00b; Baudry »02d]). Les travaux qui se sont intéressés à la production de l’oracle pour le test unitaire de classes, sont tous fondés sur la spécification de post-conditions pour les méthodes de la classe [Edwards »01; Cheon »02; Fenkam »02]. Ces travaux sont détaillés dans la section 2.5 sur l’utilisation des contrats pour le test.

Cours gratuitTélécharger le cours complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *