Scott Dockendorf
Telligent Systems, Inc.
Septembre 2005
S'applique à :
Microsoft Visual Studio 2005 Team System Beta 2
Team Architect & Team Test Editions
Microsoft Visual C# 2005
Scott examine en détail la base des tests unitaires automatisés et le moteur de génération de code inclus dans l'infrastructure de tests unitaires fournie par Microsoft Visual Studio 2005 Team System. (20 pages imprimées)
Consultez l'article en anglais
Sur cette page
Introduction
Repenser les tests unitaires
Description des tests unitaires automatisés
Pourquoi générer du code ?
Exemple de génération de code
Qu'est-ce qui a été généré ?
Après la génération : que faut-il faire ?
Régénération du code de tests unitaires
Recommandation pour les tests unitaires automatisés
Conclusion
A propos de l'auteur
Introduction
Face à l'innovation et au développement des sociétés, les systèmes changent. Or, ces systèmes continuent de croître en complexité à mesure que l'entreprise évolue, innove et intègre des partenaires, des clients, des fournisseurs.
Cette complexité oblige les responsables informatiques à s'assurer de la qualité durant le processus de développement, c'est-à-dire avant la mise en œuvre. Une solution consiste à demander aux développeurs de réduire le nombre de défauts incorporés dans le cycle d'assurance qualité, en exécutant avec la plus grande rigueur des tests unitaires automatisés par rapport à votre code personnalisé. L'introduction obligée de tests unitaires automatisés dans le cycle de développement fournit aux membres d'une équipe des exemples simples et documentés d'utilisation du code personnalisé.
Un des problèmes spécifiques de la mise en place de tests unitaires automatisés et structurés vient de la quantité de code nécessaire pour l'accomplissement de ces tâches. (Il faut beaucoup de code pour tester votre code !) Le concept de générateur de code, simplement défini comme « logiciel qui crée du logiciel », s'impose de plus en plus au sein des bureaux de développement informatique des entreprises. Certains pensent que la génération de code contribue à réduire les délais de commercialisation, à imposer l'application des conventions et des normes internes et à faire évoluer le processus de développement.
Consciente de ce besoin, Microsoft a inclus dans son nouvel outil de développement, Visual Studio 2005 Team System (VSTS), un moteur de génération de code riche de fonctionnalités. Cet article explique les différentes étapes à suivre pour générer le code de tests unitaires et comment l'utiliser à un rythme régulier.
Repenser les tests unitaires
Considérons la situation : vous êtes chargé de mettre en place le système nouvelle génération de votre entreprise et vous êtes membre d'une importante équipe de développement. Vous êtes développeur d'interfaces utilisateur et vous devez produire autant de formulaires Microsoft ASP.NET/Microsoft WinForms que vous le pouvez. Vous confiez à l'équipe appropriée la réalisation des composants de la couche intermédiaire, destinés à effectuer les opérations CRUD (Create-Retrieve-Update-Delete) de la base de données et à mettre en application les règles métier associées à chaque entité du système.
Après des semaines de développement de l'interface, les formulaires sont terminés et les développeurs des composants intermédiaires vous informent qu'ils sont prêts à fournir les bibliothèques de classes. Le tableau 1 montre un exemple de dialogue que nous sommes nombreux à avoir tenu au cours de notre carrière.
Tableau 1. Exemple de dialogue entre le développeur d'interface utilisateur et les développeurs du niveau intermédiaire
Niveau intermédiaire : « Les objets sont prêts à l'emploi, il suffit de prendre la dernière version de OurSystemBL.dll et tout devrait marcher. »
Interface utilisateur : « Merci. Avez-vous une documentation que nous pourrions réviser ? »
Niveau intermédiaire : « LOOOOL ! C'est-à-dire que... ! On a tout juste eu le temps pour le code ! Voyez le document de conception... Oh ! attendez, il n'est pas fini non plus... (il va falloir s'y mettre bientôt !) »
Interface utilisateur : « Avez-vous utilisé la documentation XML ? »
Niveau intermédiaire : « Sur les constructeurs, mais pas vraiment pour les méthodes. »
Interface utilisateur : « Et un exemple de code qui montre comment créer, exécuter et décomposer l'objet ? »
Niveau intermédiaire : « J'ai mis un exemple d'application WinForms (extrait de mon poste de travail) qui devrait faire l'affaire... Mais ce n'est pas dans Microsoft Visual SourceSafe. »
Après vous être demandé comment vous avez pu récupérer un projet aussi fun, vous marmonnez quelques mots bien appuyés et vous décidez de vérifier la suite des tests unitaires des développeurs « intermédiaires ». En examinant le code, vous remarquez que le formulaire contient deux zones de texte sans libellé et trois boutons marqués button1, button2 et button3 (avec un peu de chance, ils sont alignés sur le formulaire). Ensuite, vous cherchez à savoir quels événements sont associés à ces boutons et vous vous apercevez que rien dans le code n'est commenté et que les variables de données sont nommées x, y et z. Heureusement, vous avez constaté que button1 et button2 exécutent la méthode Save() de l'objet et que button3 exécute la méthode Delete(). Lors de l'exécution, vous recevez un paquet d'erreurs System.Exception car il vous manquait plusieurs paramètres de configuration.
Il s'agit là d'un cas extrême et j'espère que les bureaux qui passent par ce genre d'expérience ne sont pas trop nombreux ; mais examinons les problèmes rencontrés dans ce scénario en ce qui concerne les tests unitairess :
-
Cette forme de code de tests unitaires n'est pas structurée : le code est injecté dans les événements clic des boutons et il est difficile à déchiffrer.
-
Cette forme de code de tests unitaires n'est pas bien documentée.
-
Cette forme de tests unitaires ne s'appuie pas sur des données dont on sait qu'elles sont bonnes ou mauvaises, tout se limite à ce qui a été entré dans des zones de texte.
-
Le code de tests unitaires ne peut pas se répéter automatiquement, sur la base du code entré.
-
La couverture du code de tests unitaires est inconnue : les données déterminent la quantité de code qui a été réellement testée.
-
Les détails d'implémentation ne font pas l'objet d'une communication claire entre les membres de l'équipe.
Description des tests unitaires automatisés
L'infrastructure xUnit a été introduite comme concept de base de eXtreme Programming en 1998. Cette méthode définissait un mécanisme efficace pour aider les développeurs à créer des tests unitaires automatisés performants et structurés lors de leurs activités courantes de développement. Depuis lors, cette infrastructure a évolué en véritable norme pour les infrastructures de tests unitaires automatisés.
Caractéristique des tests unitaires automatisés
En bref, les tests unitaires automatisés sont :
-
structurés ;
-
auto-documentés ;
-
automatiques et répétables ;
-
basés sur des données connues ;
-
conçus pour tester des actions positives et négatives ;
-
idéals pour tester une implémentation sur différentes machines ;
-
constituent des exemples de configuration, d'implémentation et d'exécution.
Éléments de l'infrastructure xUnit
Le tableau 2 récapitule les concepts de base de l'infrastructure xUnit et les équivalents de l'infrastructure de tests unitaires de Visual Studio 2005 Team System.
Tableau 2. Correspondance entre l'infrastructure xUnit et les concepts de l'infrastructure de tests unitaires VSTS
| Concept de l'infrastructure xUnit | Équivalent VS 2005 (voir les attributs ci-dessous) | Description |
| Test | TestMethod | Ce sont vos tests. Fournit la logique pour vérifier que le résultat est bien celui prévu et vous informe si le résultat n'est pas atteint. Imaginez qu'il s'agit de votre « méthode ». |
| Contexte du test (Fixture) | TestClass | Regroupement logique d'un à plusieurs tests. Imaginez qu'il s'agit de votre « classe ». |
| Suite de tests | Test List ** | Regroupement logique d'un à plusieurs contextes de test. Imaginez qu'il s'agit de votre « bibliothèque de classes ».
Remarque : Cette liste n'a pas besoin d'attribut. |
| Testeur | Infrastructure de tests unitaires VS 2005 VSTS | GUI/Application de console chargée de découvrir, d'exécuter et de présenter les résultats des tests. Dans cet article, Visual Studio 2005 Team System servira de testeur. |
Exemple de contexte de test
Examinons le diagramme suivant pour la classe BankAccount et un exemple de contexte de test (BankAccountTests.cs).
Figure 1. Classe BankAccount
Exemple de contexte de test : BankAccountTests.cs
using BankAccountDemo.Business;
using Microsoft.VisualStudio.QualityTools.UnitTesting.Framework;
namespace BankAccountDemo.Business.Tests
{
[TestClass()]
public class BankAccountTest
{
[TestInitialize()]
public void Initialize() {
}
[TestCleanup()]
public void Cleanup() {
}
[TestMethod()]
public void ConstructorTest() {
float currentBalance = 500;
BankAccount target = new BankAccount(currentBalance);
Assert.AreEqual(currentBalance, target.CurrentBalance,
"Balances are not equal upon creation");
}
[TestMethod()]
public void DepositMoneyTest() {
float currentBalance = 500;
BankAccount target = new BankAccount(currentBalance);
float depositAmount = 10;
target.DepositMoney(depositAmount);
Assert.IsTrue( (currentBalance + depositAmount) >
target.CurrentBalance,
"Deposit not applied correctly");
}
[TestMethod()]
public void MakePaymentTest() {
float currentBalance = 500;
BankAccount target = new BankAccount(currentBalance);
float paymentAmount = 250;
target.MakePayment(paymentAmount);
Assert.IsTrue(currentBalance - paymentAmount ==
target.CurrentBalance,
"Payment not applied correctly");
}
}
}
Concept de test unitaire principal == Assertions
Le principal concept utilisé pour cette forme de test est que les tests unitaires automatisés sont basés sur des « assertions », que l'on peut définir comme étant « la vérité, ou ce que vous pensez être la vérité ». D'un point de vue logique, considérez l'instruction suivante : «lorsque je fais {x}, j'attends {y} comme résultat ».
Ceci se convertit très facilement en code, avec une des trois classes d'assertion disponibles dans l'espace de noms Microsoft.VisualStudio.QualityTools.UnitTesting.Framework : Assert, StringAssert et CollectionAssert. La classe principale, Assert, fournit des assertions utilisées pour tester les instructions conditionnelles fondamentales. La classe StringAssert contient des assertions personnalisées utiles pour travailler sur des variables de chaîne. De même, la classe CollectionAssert inclut des méthodes d'assertion utiles pour travailler sur des collections d'objets.
Le tableau 3 montre les assertions disponibles dans la version actuelle de l'infrastructure de tests unitaires.
Tableau 3. Assertions de l'infrastructure de tests unitaires
| Classe Assert | Classe StringAssert | Classe CollectionAssert |
| AreEqual() | Contains() | AllItemsAreInstancesOfType() |
| AreNotEqual() | DoesNotMatch() | AllItemsAreNotNull() |
| AreNotSame() | EndsWith() | AllItemsAreUnique() |
| AreSame() | Matches() | AreEqual() |
| EqualsTests() | StartsWith() | AreEquivalent() |
| Fail() |
| AreNotEqual() |
| GetHashCodeTests() |
| AreNotEquivalent() |
| Inconclusive() |
| Contains() |
| IsFalse() |
| DoesNotContain() |
| IsInstanceOfType() |
| IsNotSubsetOf() |
| IsNotInstanceOfType() |
| IsSubsetOf() |
| IsNotNull() |
|
|
| IsNull() |
|
|
| IsTrue() |
|
|
Que testez-vous ?
Ces tests unitaires automatisés sont de tout premier niveau. Ils ont pour but de tester vos objets jusqu'au niveau du constructeur, de l'appel de méthode et même d'une propriété de l'objet.
La question « tests publics / tests privés » est largement débattue dans les milieux spécialisés. Pour beaucoup, les tests unitaires doivent tester uniquement l'interface publique d'un objet. D'autres estiment que chaque appel doit être testé, y compris les méthodes internes privées. VSTS prend en charge les deux niveaux de tests unitaires. VSTS gère les tests privés à l'aide d'accesseurs privés ou de classes wrapper qui permettent de générer des tests unitaires basés sur des propriétés et des méthodes « privées ».
Quel composant exécute ces tests unitaires automatisés ?
Comme indiqué plus haut, l'infrastructure xUnit définit le concept de « testeur » comme application chargée (a) d'exécuter les tests unitaires et (b) de présenter les résultats. Dans le cadre de cet article, le moteur de tests unitaires inclus avec Visual Studio 2005 Team System (VSTS) sert de « testeur ». La figure 2 représente les résultats de l'exécution pour la classe BankAccountTests.cs.
Figure 2. Volet Test Results : Résultats d'exécution des tests unitaires
Microsoft Visual Studio 2005 remplit dynamiquement cette vue en utilisant le modèle de code du projet source. Il détermine dynamiquement les informations relatives à la suite de tests, sur la base d'attributs personnalisés figurant dans le code source. Le tableau 4 présente les attributs des tests unitaires les plus courants (ainsi que l'ordre d'exécution).
Tableau 4. Attributs courants des tests unitaires
| Attribut | Description |
| TestClass() | Cet attribut désigne un contexte de test. |
| TestMethod() | Cet attribut désigne un cas de test. |
| AssemblyInitialize() | Les méthodes ayant cet attribut sont exécutées avant l'exécution du premier TestMethod() dans le premier TestClass() sélectionné pour exécution. |
| ClassInitialize() | Les méthodes ayant cet attribut sont appelées avant l'exécution du premier test. |
| TestInitialize() | Les méthodes ayant cet attribut sont appelées avant l'exécution de chaque TestMethod(). |
| TestCleanup() | Les méthodes ayant cet attribut sont appelées après l'exécution de chaque TestMethod(). |
| ClassCleanup() | Les méthodes ayant cet attribut sont appelées après l'exécution de TOUS les tests. |
| AssemblyCleanup() | Les méthodes ayant cet attribut sont exécutées après l'exécution du dernier objet TestMethod() dans le premier objet TestClass() sélectionné pour exécution. |
| Description() | Fournit une description pour un TestMethod() donné. |
| Ignore() | Ignore un TestMethod() ou un TestClass() pour une raison quelconque. |
| ExpectedException() | En cas de test d'une exception spécifique, l'exception spécifiée avec cet attribut n'entraînera pas d'échec si elle est générée depuis le code d'implémentation. |
Quels types de tests écrire ?
Vous aurez rarement une relation un à un entre une méthode et les tests associés. Lorsqu'ils écrivent des tests unitaires automatisés, les développeurs doivent tout envisager et tout savoir sur votre objet : comment il sera utilisé, exploité, éliminé, et de quelle manière il réagit, positivement, négativement ou sans résultats, dans toutes les circonstances.
Par exemple, prenez un objet standard destiné à exécuter les fonctions CRUD (create, retrieve, update, delete) pour les entrées Clients d'une base de données. Pour la méthode Load() de cet objet, vous allez écrire des tests portant sur les scénarios suivants :
-
Constructor Test : pour garantir que votre objet se charge correctement, avec les informations adéquates.
-
PositiveLoadScalarTest : pour vérifier le chargement correct d'un client existant dans la base de données.
-
NegativeLoadScalarTest : pour tester le chargement non réussi d'un client, à savoir un client qui n'existe pas dans la base de données.
-
PositiveLoadTest : pour tester le chargement correct des clients, sur la base de données connues.
-
NegativeLoadTest : pour tester le chargement non réussi des clients qui n'existent pas dans la base de données.
-
NegativeValidationTest : pour vérifier que votre logique de validation fonctionne correctement.
Ce ne sont là que quelques-unes des nombreuses utilisations possibles d'une suite de tests unitaires automatisés. J'ai entendu parlé un jour d'un bureau qui utilisait des tests unitaires pour vérifier l'existence d'attaques de sécurité connues sur ses composants. Dans une perspective plus large, les tests unitaires doivent très clairement garantir vos composants en vue d'une utilisation normale. Le fait davoir un jeu de tests bien rodés donne à votre équipe l'assurance que vous avez réalisé exactement ce que vous vouliez : écrire un logiciel efficace. Quel que soit le prix à payer pour cette confiance, vous devez écrire ces tests.
Pourquoi générer du code ?
La lecture de la liste ci-dessus vous remémore sans doute les objets monolithiques des anciens projets et vous amène à penser que si vous deviez procéder ainsi avec ces objets-là, il y aurait quantité de code à écrire ! Mais dites-vous que les développeurs écrivent toujours du code de tests unitaires, simplement sous d'autres formes (via des formulaires WinForms par exemple, comme je l'ai indiqué plus haut). De plus, l'avantage de disposer d'un exemple d'implémentation documenté et réutilisable compense l'inconvénient de devoir générer plus de code. Enfin, il est avéré que le fait de passer davantage de temps à la conception de tests unitaires minutieux contribue réellement à réduire les défauts durant le cycle de contrôle qualité.
Comme indiqué plus haut, la génération de code se définit comme le processus logiciel qui crée du logiciel. Il s'agit d'une solution idéale pour créer du code sur la base de processus reproductibles. Par exemple, la génération de code s'avère utile dans les cas suivants : script de données, création d'objets qui représentent des entités et leur persistance dans un référentiel (CRUD de base de données) ou création de contrôles d'interface utilisateur orientés vers la maintenance des données. Citons parmi les principaux avantages de la génération de code :
-
Gain de temps : pourquoi passer des heures, des jours, des semaines à créer quelque chose que vous pouvez créer en quelques secondes ou quelques minutes ?
-
Mise en oeuvre de normes et de conventions : rien n'est mieux, pour imposer des normes et des conventions de nommage, que de supprimer l'intervention humaine dans la phase de développement et de recourir à un processus reproductible sur la base de vos propres règles.
-
Capacité à tester des méthodes privées : comme indiqué plus haut, l'infrastructure de tests unitaires permet de tester des méthodes privées à l'aide des classes « private accessor ». Le moteur de génération de code crée tout le code de base associé à ces classes accesseur.
-
Connaissance approfondie des composants existants : vous voulez en savoir plus sur les composants d'un autre développeur ? La génération de code sur la base de ces composants peut permettre une implémentation rapide et fournir les interfaces de l'objet. De plus, les développeurs qui conçoivent et rendent accessibles les interfaces publiques de leur objet (en utilisant VS 2005 Class Designer, par exemple) avant le codage tireront largement parti du moteur de génération de code.
Comme vous pouvez vous y attendre, l'écriture de tests unitaires automatisés fait partie des tâches pour lesquelles la génération de code convient. Est-ce que vous n'apprécieriez pas de pouvoir, à partir de composants déjà existants, générer le code initial des tests unitaires automatisés ? Et générer, outre le code de l'infrastructure des tests, un exemple d'implémentation construit autour de l'interface publique d'un objet ? Les futurs propriétaires de Visual Studio 2005 Team System en auront la possibilité, sans parler d'autres avantages.
Exemple de génération de code
Dans cet exemple, nous allons générer du code pour la classe BankAccount présentée plus haut. Cette partie de l'article décrit le processus de génération et a pour but de mettre en évidence les fonctionnalités proposées ainsi que les avantages à utiliser le moteur de tests unitaires de VSTS.
Étape 1 : Création de votre code d'implémentation
Tout d'abord, créons un projet de bibliothèque de classes qui servira de couche métier pour votre application.
Pour créer cette bibliothèque dans VS 2005 :
-
Démarrez Visual Studio 2005 Beta 2.
-
Dans le menu Fichier, cliquez sur Nouveau, puis sur Projet.
-
Sélectionnez la langue de votre choix, Windows, puis choisissez le modèle de projet Bibliothèque de classes.
-
Attribuez aux zones Nom et Nom de solution la valeur BankAccountDemo.Business, sélectionnez un emplacement, puis cliquez sur OK pour créer la bibliothèque de classes.
Une fois que VS 2005 a créé la classe, la tâche suivante consiste à créer la classe BankAccount conçue pour votre projet. Pour ce faire :
-
Dans l'Explorateur de solutions, cliquez avec le bouton de la souris et sélectionnez Supprimer pour supprimer le fichier du projet et le retirer du disque.
-
Avec le bouton droit de la souris, cliquez sur le projet BankAccountDemo.Business, cliquez sur Ajouter, puis sur Classe.
-
Choisissez le nom de fichier BankAccount.cs et cliquez sur Ajouter pour créer le fichier de classe.
-
Modifiez le code du fichier BankAccount.cs comme suit.
using System;
using System.Collections.Generic;
using System.Text;
namespace BankAccountDemo.Business
{
public class BankAccount
{
// Properties
private float _currentBalance;
public float CurrentBalance
{
get { return _currentBalance; }
}
// Constructors
public BankAccount(float initialBalance)
{
this._currentBalance = initialBalance;
}
// Methods
public void DepositMoney(float depositAmount)
{
this._currentBalance += depositAmount;
}
public void MakePayment(float paymentAmount)
{
this._currentBalance -= paymentAmount;
}
}
}
Étape 2 : Génération du code initial du test unitaire
Avec le moteur de tests unitaires intégré de Visual Studio 2005 Team System, le processus de génération de code est plus facile que jamais. Outre la structure de test, il génère les informations spécifiques d'instance, telles que les données de création de l'objet, les paramètres typés et l'exécution des méthodes.
VS 2005 permet de générer du code de test unitaire à n'importe quel niveau structurel de la classe : espace de noms, classe, méthode, propriété, constructeur, etc. Pour ce faire, cliquez avec le bouton droit de la souris sur les éléments de code concernés et choisissez Generate test(s) (Générer les test(s)) (figure 3).
Figure 3. Méthode de génération de test
Ainsi, pour commencer le processus de génération du code, procédez comme suit :
Cliquez avec le bouton droit de la souris sur le nom de la classe, BankAccount, puis sélectionnez Create Tests (Créer les tests).
Vous devez obtenir la boîte de dialogue Generate Unit Tests (Générer les tests unitaires) (figure 4). Cette boîte et ses composants vous permettent de personnaliser le code généré durant le processus. Examinons chacun de ces éléments.
Figure 4. Boîte de dialogue Generate Unit Tests
L'arborescence Sélection actuelle : permet de naviguer dans la classe personnalisée et ses éléments. VS 2005 utilise la réflexion pour alimenter cette arborescence et sélectionne automatiquement les composants, en fonction du niveau sur lequel vous avez cliqué avec le bouton droit avant de choisir Create Tests (Créer les tests). Dans la figure 3, étant donné que j'ai opéré au niveau classe, tous les éléments de classe pour la génération de code ont été automatiquement sélectionnés. Si vous sélectionnez une génération à des niveaux individuels (constructeur, propriété ou méthode), seuls ces éléments seront sélectionnés.
L'option Filter (Filtrer), dans l'angle supérieur droit, permet de modifier les résultats affichés dans l'arborescence (figure 5), y compris l'affichage d'éléments non publics, des types de base et de « My code only ». Ceci est intéressant si vous travaillez sur une solution de grande envergure ou si l'affichage de vos structures internes privées finit par encombrer la fenêtre de sélection.
Figure 5. Filtrage des résultats de la sélection
L'affichage suivant est la zone de liste Output project (Projet résultant), située sous l'arborescence Current selection (Sélection actuelle). Cette liste vous permet de sélectionner le project destinataire pour les contextes de test générés (figure 6). Si votre solution contient un projet de test déjà créé, celui-ci sera inclus dans la sélection. Comme il s'agit de notre premier accès à la boîte de dialogue, les options Create a new Test Project (Créer un nouveau projet de test) sont disponibles.
Figure 6. Sélection du projet résultant
Pour continuer le processus :
Pour finir, la boîte de dialogue permet de personnaliser le processus de génération de code grâce au bouton Paramètres, situé dans l'angle inférieur gauche. Le fait de cliquer sur ce bouton ouvre la boîte de dialogue Test Generation Settings (Paramètres de génération de tests) (figure 7).
Figure 7. Boîte de dialogue Test Generation Settings
Dans cette boîte, vous pouvez effectuer les modifications suivantes :
-
Modifier les conventions de nommage utilisées pour générer les noms de fichier, de classe (contexte de test) et de méthode (test).
-
Activer/désactiver la possibilité de marquer tous les résultats de test non concluants par défaut. Le fait de sélectionner cette option inclut l'instruction d'espace réservé suivante dans chaque méthode Test().
Assert.Inconclusive("TODO: Implement code to verify target"); -
Activer/désactiver la génération d'avertissement, pour permettre la présentation des avertissements qui peuvent se produire durant la génération du code.
-
Qualifier globalement tous les types. Ce paramètre indique au moteur de génération du code qu'il doit ajouter un qualificateur global (global:: dans Microsoft Visual C# 2005) dans la déclaration de variable. Utilisez cette option lorsque vous avez des objets de même nom qui résident dans plusieurs espaces de noms. À défaut, le moteur de génération de code produit une logique pour créer l'objet, mais le compilateur ne pourra pas déterminer quelle classe créer et il générera une erreur.
-
Activer/désactiver de génération d'un test pour des éléments qui ont déjà des tests. Nous aborderons plus loin ce sujet des tentatives de génération de code consécutives.
-
Activer/désactiver les commentaires de la documentation. Ceci vous permet de désactiver la création d'une documentation XML avant chaque méthode Test().
Pour terminer notre configuration et générer notre code de test unitaire (et plus) :
-
Cliquez sur le bouton OK pour lancer le processus de génération.
-
Entrez le nom BankAccountDemo.Business.Test comme nom du nouveau projet et cliquez sur Create (Créer) pour terminer le processus.
VS 2005 affiche une barre de progression indiquant l'avancement du processus. Au bout de quelques secondes, le processus se termine et vous devez voir une classe appelée BankAccountTest.cs.
Qu'est-ce qui a été généré ?
Avant de revoir plus particulièrement le contexte des tests, examinons ce qui a été créé durant le processus de génération de code.
Tout d'abord, le projet Bibliothèque de classes de tests BankAccountDemo.Business.Test a été créé. Notez que le projet contient les références de votre classe d'implémentation BankAccountDemo.Business (à partir de laquelle vous avez généré le code) et la bibliothèque de classes Microsoft.VisualStudio.QualityTools.UnitTestFramework. En examinant le contenu de cette classe, vous remarquez les fichiers suivants :
-
AuthoringTests.txt : il s'agit d'un contenu informatif qui définit comment utiliser les tests unitaires (ouverture, affichage, exécution, visualisation des résultats, modification du mode d'exécution) et propose des définitions des différents types de test inclus dans VSTS.
-
ManualTest1.mht : il s'agit du fichier qui contrôle les tests manuels utilisés dans VSTS pour exécuter les tests et afficher les résultats. Les tests manuels sont un type supplémentaire de test pris en charge par VSTS. Pour plus d'informations, reportez-vous à la bibliothèque MSDN, à la rubrique Tests manuels.
-
UnitTest1.cs : ce fichier est une classe de référence qui fournit simplement un test unitaire de base, avec des définitions pour TestClass, TestInitialize, TestCleanup et TestMethod.
-
BankAccountTest.cs : ce fichier contient le code de test unitaire généré, spécifique de votre assembly. Regardons ce code de plus près car il est le plus important du processus de génération.
La classe générée par le moteur de tests unitaires inclut les composants suivants :
-
Instructions Using/imports pour les assemblys référencés.
-
Définition TestClass() pour la classe qui contient le test (BankAccountTestFixture).
-
Un accesseur privé et une propriété publique pour TestContext. Ceux-ci sont utilisés par le testeur (c'est-à-dire par l'infrastructure de tests unitaires VSTS) pour fournir des informations sur l'exécution en cours et les fonctionnalités nécessaires au testeur.
-
Méthodes TestInitialize() et TestCleanup(). Celles-ci sont couramment utilisées pour acquérir et libérer les objets nécessaires aux tests.
-
TestMethod() pour chaque méthode sélectionnée.
Étudions de plus près la méthode DepositMoneyTest(), qui doit garantir que le solde actuel correspond bien à la somme d'origine plus le montant du dépôt.
///
///A test case for DepositMoney (float)
///
[TestMethod()]
public void DepositMoneyTest()
{
float initialBalance = 0; // TODO: Initialize to an appropriate value
BankAccount target = new BankAccount(initialBalance);
float depositAmt = 0; // TODO: Initialize to an appropriate value
target.DepositMoney(depositAmt);
Assert.Inconclusive("A method that does not return a value" +
"cannot be verified.");
}
Vous pouvez constater que le moteur de génération de code ne s'est pas contenté de créer un objet TestMethod() contenant du code. Il a créé des exemples de tests unitaires adaptés à votre interface, notamment :
-
Affectation et construction de l'objet BankAccount (l'objet du test).
-
Création et affectation par défaut de variables locales qui représentent les paramètres nécessaires pour la méthode/le constructeur faisant l'objet du test.
-
Commentaires TODO, rappelant au développeur la nécessité d'affecter les variables de paramètre de façon appropriée.
-
Si le test s'appuie sur un appel de méthode de l'objet source, le code généré va contenir l'appel à cette méthode, avec des variables locales pour les paramètres.
-
L'appel de méthode Assert() initial, basé sur les valeurs renvoyées de la méthode.
-
L'appel de méthode Assert.Inconclusive(), comme rappel à exécuter le code du test. Les tests non concluants s'affichent comme des échecs dans la boîte de dialogue Test Results.
Après la génération : que faut-il faire ?
Les avantages de la génération de code deviennent souvent évidents lorsque vous examinez ce que vous n'aviez pas à faire pour accomplir la même tâche. Dans notre exemple, nous n'avions pas à :
-
créer le projet de tests unitaires ;
-
définir les références du projet ;
-
ajouter les classes de tests appropriées ;
-
établir les classes et les attributs de base de l'infrastructure de tests unitaires ;
-
créer des méthodes de test individuelles ;
-
créer la logique spécifique de l'interface.
Étant donné que le processus de génération de code a créé des exemples de tests unitaires adaptés à notre interface, nous ne devrions pas être loin de terminer notre test initial. Plus précisément, il suffit à présent de « combler les vides » et d'exécuter les assertions, en attribuant des « valeurs de données connues » à vos variables de propriétés et de créer les méthodes Assert() adéquates. Évidemment, ce ne sera pas le cas pour tous les tests, en particulier les tests complexes avec assertions multiples.
En quelques secondes seulement (et un minimum de saisie), vous devez pouvoir convertir le code de test unitaire généré en tests réels.
Par exemple, imaginez que nous ayons commencé par celui-ci :
[TestMethod()]
public void DepositMoneyTest()
{
float initialBalance = 0; // TODO: Initialize to an appropriate value
BankAccount target = new BankAccount(initialBalance);
float depositAmt = 0; // TODO: Initialize to an appropriate value
target.DepositMoney(depositAmt);
Assert.Inconclusive("A method that does not return a value " +
"cannot be verified.");
} Nous pouvons désormais le compléter assez facilement et en quelques lignes (modifications indiquées en gras).
[TestMethod()]
public void DepositMoneyTest() {
float currentBalance = 500;
BankAccount target = new BankAccount(currentBalance);
float depositAmt = 10;
target.DepositMoney(depositAmt);
Assert.AreEqual(currentBalance + depositAmt, target.CurrentBalance,
"Deposit Test: Deposit not applied correctly");
}
Régénération du code de tests unitaires
Il est intéressant de remarquer que le processus de génération de code n'écrase pas les tests unitaires préalablement générés (et modifiés). Avec la version bêta 2 de Visual Studio 2005 Team System, les options de génération de code permettent d'activer ou désactiver la création des tests déjà existants. Si l'option est activée et si le processus détecte une méthode de test existante portant le même nom, il n'y touche pas et crée des tests consécutifs, en ajoutant un numéro à la fin du nom de méthode. Ceci se produit couramment en cas d'utilisation de méthodes surchargées ou de constructeurs figurant dans votre objet, ou encore quand vous cliquez sur le bouton Générer sans désélectionner les tests existants.
Recommandation pour les tests unitaires automatisés
Cette section pourrait faire l'objet d'un article à part entière, mais je vais rappeler quelques recommandations essentielles à suivre lorsque vous créerez des tests unitaires.
-
Vous devez concevoir des tests indépendants les uns des autres, capables de s'exécuter de façon autonome (puisqu'ils peuvent être sélectionnés ou désélectionnés à volonté par l'interface de test).
-
Ne testez pas uniquement les éléments positifs. Assurez-vous que le code répond à tous les scénarios, y compris lorsqu'un événement imprévu se produit (ressources non disponibles, base de données en lecture seule, etc.).
-
Placez-vous du point de vue de l'assurance qualité et raisonnez en vérificateur, pas seulement en développeur. Le temps que vous passez à concevoir vos tests unitaires vous aidera à réduire le temps passé ultérieurement à résoudre les problèmes. Concentrez-vous sur les détails de vos objets : comment les données sont-elles transférées entre eux ? Qui les utilise ? Est-il facile de casser l'objet ? Que se passe-t-il si je fais telle ou telle chose ?
-
Pensez « de l'extérieur ». Prenez le temps d'imaginer autant de tests que vous le pouvez. Lorsque vous avez terminé, prenez un peu de recul et vérifiez tout ce que vous auriez pu oublier. Demandez aux membres de votre équipe de vous faire part de leurs commentaires ; par exemple, quels autres types de tests créent-ils ? Un regard extérieur permet à un développeur, familier avec son propre code, d'avoir une perspective différente.
-
Couverture du code. Utilisez le mécanisme de Couverture de code de VSTS pour fournir des informations sur la quantité de code réellement exécuté durant chaque test (nombre de lignes de code, pourcentage par rapport à la totalité). Si le codage est terminé et si tous vos tests ont abouti alors que la couverture montre l'exécution d'une logique restreinte, demandez-vous si les tests ont réellement réussi. Une couverture élevée ne signifie pas nécessairement que vous avez un jeu complet de tests, mais le code non couvert est généralement tout désigné pour un nouveau cas de test.
-
Lorsque vous créez vos tests unitaires, pour aider les autres utilisateurs à découvrir votre code :
- Utilisez une structure de projet reflétant la structure de l'assemblage testé.
Chaque assemblage a un assemblage de test relié.
Chaque classe a une classe de test reliée.
Incluez chaque nom de méthode dans les méthodes de test respectives (ainsi, Load() aura des méthodes de tests de
PositiveLoadTest(), NegativeLoadTest(), PositiveScalarLoadTest(), et ainsi de suite).
- Utilisez des conventions de nommage cohérentes, y compris pour les noms des méthodes et des propriétés de l'objet.
-
Et si tout le reste échoue, déboguez ! Les tests unitaires automatisés devraient contribuer à réduire le temps passé au débogage. Toutefois, si des résultats des tests et la couverture de code ne vous permettent pas de savoir pourquoi le test échoue, n'hésitez pas à les déboguer. À partir de la version bêta 2 de Visual Studio 2005 Team System, les développeurs peuvent déboguer les assembly de tests unitaires en utilisant l'option Debug checked tests (Déboguer les tests) de Test Manager.
Conclusion
Les tests unitaires automatisés proposent pour votre cycle de développement un processus structuré, documenté, reproductible et extrêmement portable. Si vous cherchez des assembly existants ou si, dans votre environnement, votre conception doit être complète pour qu'il soit possible de démarrer le développement, pensez à utiliser le moteur de génération de code intégré dans Microsoft Visual Studio 2005 Team System. La fonctionnalité de génération de code de tests unitaires dans Visual Studio 2005 Team System vous fera très certainement gagner du temps et vous aidera à imposer des normes et des conventions de développement. En générant les éléments de base de vos tests unitaires automatisés, y compris des méthodes de test avec création d'objet, variables de paramètre et classes d'assertion de base, vous êtes en bonne voie pour incorporer la pratique des tests unitaires automatisés dans votre méthodologie de développement.
A propos de l'auteur
Directeur des services professionnels pour les systèmes telligent, Scott Dockendorf est spécialisé dans la mise en oeuvre d'applications performantes et évolutives dans .NET. Scott est passionné par l'artichecture des solutions, le développement sécurisé et les conseils aux entreprises pour qu'elles adoptent des normes et des méthodologies fiables. Scott est un membre actif de la communauté .NET et consacre du temps au Groupe des utilisateurs .NET de North Dallas (en anglais) en tant que directeur des programmes. Il anime également des sessions du groupe d'utilisateurs .NET local et prend une part active aux activités de l'International Academic Committee de l'INETA au Texas. Vous pouvez le contacter par e-mail à l'adresse scottd@telligent.com, ou sur son blog à l'adresse http://weblogs.asp.net/scottdockendorf.