|
PEX (Program EXplorer) est un programme développé par Microsoft Research, simple d’utilisation, qui s’intègre à Visual Studio ; il a pour objectif l’exploration des méthodes de vos classes, selon différentes approches :
• génération “arbitraire” de données ;
• évaluation des chemins conditionnels (path condition) ;
• évaluation des conditions de retour (flipped condition). |
Ce programme est donc particulièrement intéressant lorsque, face à une bibliothèque de classes, on hésite sur les tests unitaires à mettre en œuvre pour couvrir ses besoins. En effet, non seulement PEX effectue un travail d’exploration et génère un rapport mettant en lumière d’éventuels dysfonctionnements d’exécution mais il peut également proposer des actions correctives et générer des tests unitaires paramétrables (PUT) qui assurent la couverture du code des méthodes à 100 % ; ces tests sont bien entendu compatibles avec MSTEST (ou d’autres frameworks de test) et peuvent donc être associés à une stratégie de build.
Pour comprendre l’intérêt de PEX, je vous propose d’illustrer son fonctionnement à travers un petit cas concret. Le scénario est le suivant : il s’agit de tester un algorithme de conversion d'un nombre romain en nombre arabe (pour plus d'informations sur le principe de construction des nombres romains voir : http://fr.wikipedia.org/wiki/Num%C3%A9ration_romaine). Nous mettons en place une démarche classique de test unitaire :
1) Vérification de la couverture de code à travers deux tests unitaires simples.
2) Adaptation des tests unitaires en associant une source de données à l'une des méthodes.
3) Mise en œuvre de PEX et analyse des résultats.
La version initiale du programme est la suivante :
1: using System;
2: using System.Text;
3:
4: namespace Tekigo.Demo.ConvertirNombreRomain
5: {
6: public static class NombreRomain
7: {
8: /// <summary>
9: /// Représenter la séquence des chiffres romains disponibles
10: /// </summary>
11: /// <remarks>l'ordre est important</remarks>
12: private static string _chiffresRomain = "MDCLXVI";
13:
14: /// <summary>
15: /// Convertir un nombre romain en nombre arabe
16: /// </summary>
17: /// <param name="nombre">nombre romain</param>
18: /// <returns>nombre arabe (format entier)</returns>
19: public static int ConvertirEnNombreArabe(string nombre)
20: {
21: int nombreArabe = 0;
22: int position = 0;
23: int positionFin = nombre.Length - 1;
24: int ordre;
25:
26: //Chaîne vide ?
27: if (nombre.Trim() == String.Empty) throw new ArgumentException("Nombre non défini.");
28:
29: //Convertir la chaîne en majuscule
30: nombre = nombre.ToUpper();
31:
32: //Evaluer les caractères définis dans le nombre romain
33: foreach (char c in nombre)
34: {
35: position = nombre.IndexOf(c, position);
36: ordre = _chiffresRomain.IndexOf(c);
37:
38: switch (c)
39: {
40: case 'M':
41: if ( (position == positionFin) || (ordre <= _chiffresRomain.IndexOf(nombre[position + 1])))
42: nombreArabe += 1000;
43: else
44: {
45: //cas de figure impossible
46: throw new ApplicationException("Nombre romain invalide.");
47: }
48: break;
49: case 'D':
50: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
51: nombreArabe += 500;
52: else
53: {
54: //cas de figure impossible
55: throw new ApplicationException("Nombre romain invalide.");
56: }
57: break;
58: case 'C':
59: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
60: nombreArabe += 100;
61: else
62: nombreArabe -= 100;
63: break;
64: case 'L':
65: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
66: nombreArabe += 50;
67: else
68: nombreArabe -= 50;
69: break;
70: case 'X':
71: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
72: nombreArabe += 10;
73: else
74: nombreArabe -= 10;
75: break;
76: case 'V':
77: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
78: nombreArabe += 5;
79: else
80: nombreArabe -= 5;
81: break;
82: case 'I':
83: if (position == positionFin || ordre <= _chiffresRomain.IndexOf(nombre[position + 1]))
84: nombreArabe += 1;
85: else
86: nombreArabe -= 1;
87: break;
88: default:
89: throw new ApplicationException("Nombre romain invalide.");
90: }
91: }
92:
93: //Retourner la valeur calculée
94: return nombreArabe;
95: }
96: }
97: }
La classe de test unitaire contient initialement deux méthodes :
1: using System;
2: using Tekigo.Demo.ConvertirNombreRomain;
3: using Microsoft.VisualStudio.TestTools.UnitTesting;
4:
5: namespace Tekigo.Demo.TestProjectConversion
6: {
7: [TestClass()]
8: public class NombreRomainTest
9: {
10: private TestContext testContextInstance;
11: public TestContext TestContext
12: {
13: get { return testContextInstance; }
14: set { testContextInstance = value; }
15: }
16:
17: /// <summary>
18: ///Test n°1 pour ConvertirEnNombreArabe, avec une valeur
19: ///</summary>
20: [TestMethod()]
21: public void ConvertirEnNombreArabeTest()
22: {
23: string nombre = "MCMLXXXIV";
24: int expected = 1984;
25: int actual;
26: actual = NombreRomain.ConvertirEnNombreArabe(nombre);
27: Assert.AreEqual(expected, actual);
28: }
29:
30: /// <summary>
31: ///Test n°2 pour ConvertirEnNombreArabe, avec levée d'exception
32: ///</summary>
33: [TestMethod(), ExpectedException(typeof(ArgumentException), "Nombre non défini.")]
34: public void ConvertirEnNombreArabeTestWithException()
35: {
36: string nombre = "";
37: int actual;
38: actual = NombreRomain.ConvertirEnNombreArabe(nombre);
39: }
40: }
41: }
Si l’on vérifie la couverture de code de ces tests, on s’aperçoit qu’elle est insuffisante (un peu inférieure à 70 %).
Pour améliorer cette couverture, la méthode ConvertirEnNombreArabeTest va maintenant être pilotée par une source de données (un fichier XML contenant une série de valeurs à convertir et le résultat attendu).
1: /// <summary>
2: ///Test n°1 pour ConvertirEnNombreArabe, avec une source de données
3: ///</summary>
4: [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\romain.xml", "valeur", DataAccessMethod.Sequential), DeploymentItem("TestProjectConversion\\romain.xml"), TestMethod()]
5: public void ConvertirEnNombreArabeTest()
6: {
7: string nombre = testContextInstance.DataRow["nombre"].ToString();
8: int expected = Convert.ToInt32(testContextInstance.DataRow["attendu"]);
9: int actual;
10: actual = NombreRomain.ConvertirEnNombreArabe(nombre);
11: Assert.AreEqual(expected, actual);
12: }
La couverture est effectivement améliorée (presque 90 %). En étudiant les blocs non couverts, on détecte deux cas qui illustrent une erreur dans notre algorithme, et qu’il convient de corriger par une levée d’exception :
Cette correction n’augmente pas pour autant la couverture. Que faire alors pour atteindre les 100 % ? La solution peut passer par un accroissement du jeu de données mais, dans cette approche, suis-je capable de déterminer toutes les valeurs nécessaires ? Et, de manière plus générale, mes tests sont-ils suffisants ? C’est ici que l’utilisation de PEX prend tout son sens.
Utilisons donc PEX pour explorer notre méthode de conversion ; il suffit pour cela de lancer le programme d’exploration sur la méthode :
Au fur et à mesure de son exploration, PEX dresse un tableau des cas de test qu’il a identifié :
Analysons, lorsque l’exploration est terminée, les résultats disponibles :
1) 226 exécutions de notre méthode ont été effectuées.
2) 13 tests sont considérés comme ayant échoué.
3) 4 limites ont été atteintes (il s’agit notamment de temps d’exécution jugés trop longs).
4) pour chaque test conservé, il est possible de voir son détail.
On peut aussi visionner un rapport au format HTML à partir du menu “Views/Open Report” (au préalable, il faut activer l’option Reports de la section Pex dans les options de configuration de Visual Studio). Ce rapport vous donne accès à différentes données : couverture, exceptions levées, limites atteintes, environnement d’exécution, etc.
Parmi les éléments mis en avant par l’exploration, on note une NullReferenceException (cas assez classique) du fait de l’absence de test du paramètre string passé à la méthode. PEX nous propose une action corrective qui, si elle est validée, vient directement s’insérer dans le code.
Certains cas de test ont fait apparaître des exceptions (en gras dans le tableau) qui appellent soit une correction soit une acceptation par PEX ; ainsi, elles ne seront plus considérées comme des anomalies par l’explorateur.
A ce stade, vous pouvez décider de conserver le ou les cas de test qui vous paraissent compléter utilement vos propres tests unitaires (voire même les remplacer). Là encore, l’opération est très simple puisqu’il suffit de sélectionner ces cas dans le tableau des résultats d’exploration puis que cliquer sur l’option Save. Après validation, PEX crée son propre projet de test la solution.
Chaque cas de test est stocké, sous forme d’un test unitaire simple, dans le fichier .g.cs. Ils peuvent désormais être intégré au processus de test ou de build (intégration à faire par l’intermédiaire d’une liste de tests).
L’intégration au suivi d’activités dans Team System n’a pas été oubliée puisqu’il est possible d’associer un résultat d’exploration à un élément de travail :
En conclusion, il apparaît clairement que l’arrivée de PEX offre des perspectives très intéressantes dans l’amélioration de la qualité des tests unitaires par sa capacité d’exploration (y compris sur des méthodes manipulant des objets, des interfaces, etc.). Son environnement d’exécution est paramétrable, notamment sur la gestion des “limites” (boundaries), un aspect particulièrement nécessaire pour tenir compte des phénomènes liés aux boucles par exemple, et éviter ainsi des explorations interminables. Bien que parfaitement intégré à Visual Studio, PEX peut aussi être lancé en ligne de commande.
Notons enfin que PEX a quelques limitations :
• exploration uniquement sur du code managé (il s’appuie sur l’IL) ;
• pas de support d’exécution concurrentielle (multi-threading) ;
• pas d’exploration sur des programmes non déterministes (ça peut se comprendre !) ;
• le code généré pour les tests est écrit uniquement en C#.
Reste cependant un point à ne pas négliger (principe aussi applicable avec n’importe quel framework de test), ce n’est pas parce qu’un cas de test produit par PEX s’exécute correctement que ce dernier ne correspond pas à une anomalie de l’algorithme ! L’exemple ci-dessous montre en effet deux résultats “valides” alors que les valeurs en entrée sont des formes d’écriture impossibles en réalité. La vigilance reste donc de mise.
Adresse de téléchargement : PEX
Extensions PEX pour d’autres frameworks de test : http://www.codeplex.com/Pex