Itération #5 : Créer des tests unitaires (VB)
par Microsoft
Dans la cinquième itération, nous rendons notre application plus facile à gérer et à modifier en ajoutant des tests unitaires. Nous modélisons nos classes de modèle de données et créons des tests unitaires pour nos contrôleurs et notre logique de validation.
Génération d’une application Gestion des contacts ASP.NET MVC (VB)
Dans cette série de tutoriels, nous créons une application de gestion des contacts complète du début à la fin. L’application Gestionnaire de contacts vous permet de stocker des informations de contact (noms, numéros de téléphone et adresses e-mail) pour une liste de personnes.
Nous créons l’application sur plusieurs itérations. À chaque itération, nous améliorons progressivement l’application. L’objectif de cette approche à itérations multiples est de vous permettre de comprendre la raison de chaque modification.
Itération #1 : créez l’application. Dans la première itération, nous créons le Gestionnaire de contacts de la manière la plus simple possible. Nous ajoutons la prise en charge des opérations de base de données de base de données : Create, Read, Update et Delete (CRUD).
Itération n°2 : rendre l’application agréable. Dans cette itération, nous améliorons l’apparence de l’application en modifiant la ASP.NET vue MVC par défaut master page et la feuille de style en cascade.
Itération n°3 - Ajouter la validation de formulaire. Dans la troisième itération, nous ajoutons la validation de formulaire de base. Nous empêcherons les utilisateurs d’envoyer un formulaire sans remplir les champs de formulaire requis. Nous validons également les adresses e-mail et les numéros de téléphone.
Itération n° 4 : rendre l’application faiblement couplée. Dans cette quatrième itération, nous profitons de plusieurs modèles de conception logicielle pour faciliter la maintenance et la modification de l’application Gestionnaire de contacts. Par exemple, nous refactorisons notre application pour utiliser le modèle référentiel et le modèle d’injection de dépendances.
Itération #5 - Créer des tests unitaires. Dans la cinquième itération, nous rendons notre application plus facile à gérer et à modifier en ajoutant des tests unitaires. Nous modélisons nos classes de modèle de données et créons des tests unitaires pour nos contrôleurs et notre logique de validation.
Itération n°6 - Utiliser le développement piloté par les tests. Dans cette sixième itération, nous ajoutons de nouvelles fonctionnalités à notre application en écrivant d’abord des tests unitaires et en écrivant du code sur les tests unitaires. Dans cette itération, nous ajoutons des groupes de contacts.
Itération n°7 - Ajouter une fonctionnalité Ajax. Dans la septième itération, nous améliorons la réactivité et les performances de notre application en ajoutant la prise en charge d’Ajax.
Cette itération
Dans l’itération précédente de l’application Gestionnaire de contacts, nous avons refactorisé l’application pour qu’elle soit plus faiblement couplée. Nous avons séparé l’application en couches de contrôleur, de service et de dépôt distinctes. Chaque couche interagit avec la couche située en dessous via des interfaces.
Nous avons refactorisé l’application pour qu’elle soit plus facile à gérer et à modifier. Par exemple, si nous avons besoin d’utiliser une nouvelle technologie d’accès aux données, nous pouvons simplement modifier la couche du dépôt sans toucher le contrôleur ou la couche de service. En rendant le gestionnaire de contacts faiblement couplé, nous avons rendu l’application plus résiliente aux changements.
Mais que se passe-t-il quand nous devons ajouter une nouvelle fonctionnalité à l’application Gestionnaire de contacts ? Ou, que se passe-t-il quand nous corrigeons un bogue ? Une vérité triste, mais bien prouvée, de l’écriture de code est que chaque fois que vous touchez du code, vous créez le risque d’introduire de nouveaux bogues.
Par exemple, un beau jour, votre responsable peut vous demander d’ajouter une nouvelle fonctionnalité au gestionnaire de contacts. Elle souhaite que vous ajoutiez la prise en charge des groupes de contacts. Elle souhaite que vous puissiez permettre aux utilisateurs d’organiser leurs contacts en groupes tels que Amis, Entreprises, etc.
Pour implémenter cette nouvelle fonctionnalité, vous devez modifier les trois couches de l’application Gestionnaire de contacts. Vous devez ajouter de nouvelles fonctionnalités aux contrôleurs, à la couche de service et au dépôt. Dès que vous commencez à modifier le code, vous risquez d’interrompre les fonctionnalités qui fonctionnaient auparavant.
Refactoriser notre application en couches distinctes, comme nous l’avons fait dans l’itération précédente, était une bonne chose. C’était une bonne chose, car elle nous permet d’apporter des modifications à des couches entières sans toucher le reste de l’application. Toutefois, si vous souhaitez rendre le code d’une couche plus facile à gérer et à modifier, vous devez créer des tests unitaires pour le code.
Vous utilisez un test unitaire pour tester une unité de code individuelle. Ces unités de code sont plus petites que les couches d’application entières. En règle générale, vous utilisez un test unitaire pour vérifier si une méthode particulière dans votre code se comporte comme prévu. Par exemple, vous créez un test unitaire pour la méthode CreateContact() exposée par la classe ContactManagerService.
Les tests unitaires d’une application fonctionnent comme un filet de sécurité. Chaque fois que vous modifiez du code dans une application, vous pouvez exécuter un ensemble de tests unitaires pour case activée si la modification interrompt les fonctionnalités existantes. Les tests unitaires permettent de modifier votre code en toute sécurité. Les tests unitaires rendent tout le code de votre application plus résilient aux changements.
Dans cette itération, nous ajoutons des tests unitaires à notre application Gestionnaire de contacts. De cette façon, dans l’itération suivante, nous pouvons ajouter des groupes de contacts à notre application sans nous soucier de la rupture des fonctionnalités existantes.
Notes
Il existe diverses infrastructures de test unitaire, notamment NUnit, xUnit.net et MbUnit. Dans ce tutoriel, nous utilisons l’infrastructure de test unitaire incluse avec Visual Studio. Toutefois, vous pouvez tout aussi facilement utiliser l’une de ces infrastructures alternatives.
Éléments testés
Dans un monde parfait, tout votre code serait couvert par des tests unitaires. Dans le monde parfait, vous auriez le filet de sécurité parfait. Vous serez en mesure de modifier n’importe quelle ligne de code dans votre application et de savoir instantanément, en exécutant vos tests unitaires, si la modification a rompu les fonctionnalités existantes.
Cependant, nous ne vivons pas dans un monde parfait. Dans la pratique, lors de l’écriture de tests unitaires, vous vous concentrez sur l’écriture de tests pour votre logique métier (par exemple, la logique de validation). En particulier, vous n’écrivez pas de tests unitaires pour votre logique d’accès aux données ou votre logique d’affichage.
Pour être utiles, les tests unitaires doivent s’exécuter très rapidement. Vous pouvez facilement accumuler des centaines (voire des milliers) de tests unitaires pour une application. Si l’exécution des tests unitaires prend beaucoup de temps, vous éviterez de les exécuter. En d’autres termes, les tests unitaires de longue durée sont inutiles pour le codage quotidien.
Pour cette raison, vous n’écrivez généralement pas de tests unitaires pour le code qui interagit avec une base de données. L’exécution de centaines de tests unitaires sur une base de données dynamique serait trop lente. Au lieu de cela, vous simulerez votre base de données et écrivez du code qui interagit avec la base de données fictive (nous abordons la simulation d’une base de données ci-dessous).
Pour une raison similaire, vous n’écrivez généralement pas de tests unitaires pour les vues. Pour tester une vue, vous devez lancer un serveur web. Étant donné que la rotation d’un serveur web est un processus relativement lent, la création de tests unitaires pour vos vues n’est pas recommandée.
Si votre vue contient une logique complexe, vous devez envisager de déplacer la logique dans les méthodes d’assistance. Vous pouvez écrire des tests unitaires pour les méthodes d’assistance qui s’exécutent sans faire tourner un serveur web.
Notes
Bien que l’écriture de tests pour la logique d’accès aux données ou la logique d’affichage ne soit pas une bonne idée lors de l’écriture de tests unitaires, ces tests peuvent être très utiles lors de la création de tests fonctionnels ou d’intégration.
Notes
ASP.NET MVC est le moteur d’affichage Web Forms. Bien que le moteur d’affichage Web Forms soit dépendant d’un serveur web, d’autres moteurs d’affichage peuvent ne pas l’être.
Utilisation d’une infrastructure d’objets fictifs
Lors de la création de tests unitaires, vous devez presque toujours tirer parti d’un framework d’objet fictif. Une infrastructure d’objets fictifs vous permet de créer des fictives et des stubs pour les classes de votre application.
Par exemple, vous pouvez utiliser une infrastructure d’objet fictif pour générer une version fictive de votre classe de dépôt. De cette façon, vous pouvez utiliser la classe de dépôt fictif au lieu de la classe de référentiel réel dans vos tests unitaires. L’utilisation du référentiel fictif vous permet d’éviter d’exécuter du code de base de données lors de l’exécution d’un test unitaire.
Visual Studio n’inclut pas d’infrastructure d’objet fictif. Toutefois, plusieurs frameworks d’objets fictifs commerciaux et open source sont disponibles pour le .NET Framework :
- Moq : cette infrastructure est disponible sous la licence BSD open source. Vous pouvez télécharger Moq à partir de https://code.google.com/p/moq/.
- Rhino Mocks : cette infrastructure est disponible sous la licence BSD open source. Vous pouvez télécharger Rhino Mocks à partir de http://ayende.com/projects/rhino-mocks.aspx.
- Typemock Isolateor : il s’agit d’un framework commercial. Vous pouvez télécharger une version d’évaluation à partir de http://www.typemock.com/.
Dans ce tutoriel, j’ai décidé d’utiliser Moq. Toutefois, vous pouvez tout aussi facilement utiliser Rhino Mocks ou Typemock Isolateor pour créer les objets Mock pour l’application Gestionnaire de contacts.
Avant de pouvoir utiliser Moq, vous devez effectuer les étapes suivantes :
- .
- Avant de décompresser le téléchargement, veillez à cliquer avec le bouton droit sur le fichier et à cliquer sur le bouton intitulé Débloquer (voir figure 1).
- Décompressez le téléchargement.
- Ajoutez une référence à l’assembly Moq à votre projet test en sélectionnant l’option de menu Projet, Ajouter une référence pour ouvrir la boîte de dialogue Ajouter une référence . Sous l’onglet Parcourir, accédez au dossier dans lequel vous avez décompressé Moq et sélectionnez l’assembly Moq.dll. Cliquez sur le bouton OK (voir la figure 2).
Figure 01 : Déblocage de Moq(Cliquer pour afficher l’image en taille réelle)
Figure 02 : Références après l’ajout de Moq(Cliquer pour afficher l’image en taille réelle)
Création de tests unitaires pour la couche de service
Commençons par créer un ensemble de tests unitaires pour notre couche de service d’application du Gestionnaire de contacts. Nous allons utiliser ces tests pour vérifier notre logique de validation.
Créez un dossier nommé Models dans le projet ContactManager.Tests. Ensuite, cliquez avec le bouton droit sur le dossier Models et sélectionnez Ajouter, Nouveau test. La boîte de dialogue Ajouter un nouveau test illustrée dans la figure 3 s’affiche. Sélectionnez le modèle Test unitaire et nommez votre nouveau test ContactManagerServiceTest.vb. Cliquez sur le bouton OK pour ajouter votre nouveau test à votre projet de test.
Notes
En général, vous souhaitez que la structure de dossiers de votre projet de test corresponde à la structure de dossiers de votre projet MVC ASP.NET. Par exemple, vous placez des tests de contrôleur dans un dossier Controllers, des tests de modèle dans un dossier Models, etc.
Figure 03 : Models\ContactManagerServiceTest.cs(Cliquez pour afficher l’image en taille réelle)
Au départ, nous voulons tester la méthode CreateContact() exposée par la classe ContactManagerService. Nous allons créer les cinq tests suivants :
- CreateContact() : teste que CreateContact() retourne la valeur true lorsqu’un contact valide est passé à la méthode .
- CreateContactRequiredFirstName() : teste qu’un message d’erreur est ajouté à l’état du modèle lorsqu’un contact avec un prénom manquant est passé à la méthode CreateContact().
- CreateContactRequiredLastName() : teste qu’un message d’erreur est ajouté à l’état du modèle lorsqu’un contact avec un nom de famille manquant est passé à la méthode CreateContact().
- CreateContactInvalidPhone() : teste qu’un message d’erreur est ajouté à l’état du modèle lorsqu’un contact avec un numéro de téléphone non valide est passé à la méthode CreateContact().
- CreateContactInvalidEmail() : teste qu’un message d’erreur est ajouté à l’état du modèle lorsqu’un contact avec une adresse e-mail non valide est passé à la méthode CreateContact().
Le premier test vérifie qu’un contact valide ne génère pas d’erreur de validation. Les tests restants case activée chacune des règles de validation.
Le code de ces tests est contenu dans la liste 1.
Listing 1 - Models\ContactManagerServiceTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactManagerServiceTest
Private _mockRepository As Mock(Of IContactManagerRepository)
Private _modelState As ModelStateDictionary
Private _service As IContactManagerService
<TestInitialize()> _
Public Sub Initialize()
_mockRepository = New Mock(Of IContactManagerRepository)()
_modelState = New ModelStateDictionary()
_service = New ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object)
End Sub
<TestMethod()> _
Public Sub CreateContact()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsTrue(result)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredFirstName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, String.Empty, "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("FirstName").Errors(0)
Assert.AreEqual("First name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredLastName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", String.Empty, "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("LastName").Errors(0)
Assert.AreEqual("Last name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidPhone()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Phone").Errors(0)
Assert.AreEqual("Invalid phone number.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidEmail()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Email").Errors(0)
Assert.AreEqual("Invalid email address.", [error].ErrorMessage)
End Sub
End Class
Étant donné que nous utilisons la classe Contact dans listing 1, nous devons ajouter une référence à Microsoft Entity Framework à notre projet de test. Ajoutez une référence à l’assembly System.Data.Entity.
La liste 1 contient une méthode nommée Initialize() décorée avec l’attribut [TestInitialize]. Cette méthode est appelée automatiquement avant l’exécution de chacun des tests unitaires (elle est appelée 5 fois juste avant chacun des tests unitaires). La méthode Initialize() crée un référentiel fictif avec la ligne de code suivante :
_mockRepository = New Mock(Of IContactManagerRepository)()
Cette ligne de code utilise l’infrastructure Moq pour générer un dépôt fictif à partir de l’interface IContactManagerRepository. Le référentiel fictif est utilisé à la place de l’entité EntityContactManagerRepository réelle pour éviter d’accéder à la base de données lors de l’exécution de chaque test unitaire. Le référentiel fictif implémente les méthodes de l’interface IContactManagerRepository, mais les méthodes ne font rien.
Notes
Lors de l’utilisation de l’infrastructure Moq, il existe une distinction entre _mockRepository et _mockRepository.Object. Le premier fait référence à la classe Mock(Of IContactManagerRepository) qui contient des méthodes permettant de spécifier le comportement du dépôt fictif. Ce dernier fait référence au référentiel fictif qui implémente l’interface IContactManagerRepository.
Le référentiel fictif est utilisé dans la méthode Initialize() lors de la création d’un instance de la classe ContactManagerService. Tous les tests unitaires individuels utilisent cette instance de la classe ContactManagerService.
La liste 1 contient cinq méthodes qui correspondent à chacun des tests unitaires. Chacune de ces méthodes est décorée avec l’attribut [TestMethod]. Lorsque vous exécutez les tests unitaires, toute méthode ayant cet attribut est appelée. En d’autres termes, toute méthode décorée avec l’attribut [TestMethod] est un test unitaire.
Le premier test unitaire, nommé CreateContact(), vérifie que l’appel de CreateContact() retourne la valeur true lorsqu’une instance valide de la classe Contact est passée à la méthode . Le test crée une instance de la classe Contact, appelle la méthode CreateContact() et vérifie que CreateContact() retourne la valeur true.
Les tests restants vérifient que lorsque la méthode CreateContact() est appelée avec un contact non valide, la méthode retourne false et le message d’erreur de validation attendu est ajouté à l’état du modèle. Par exemple, le test CreateContactRequiredFirstName() crée une instance de la classe Contact avec une chaîne vide pour sa propriété FirstName. Ensuite, la méthode CreateContact() est appelée avec le contact non valide. Enfin, le test vérifie que CreateContact() retourne false et que l’état du modèle contient le message d’erreur de validation attendu « Le prénom est requis ».
Vous pouvez exécuter les tests unitaires dans la liste 1 en sélectionnant l’option de menu Tester, Exécuter, Tous les tests dans la solution (CTRL+R, A). Les résultats des tests sont affichés dans la fenêtre Résultats des tests (voir figure 4).
Figure 04 : Résultats des tests (cliquer pour afficher l’image en taille réelle)
Création de tests unitaires pour les contrôleurs
ASP.NET application MVC contrôle le flux d’interaction utilisateur. Lors du test d’un contrôleur, vous souhaitez vérifier si le contrôleur retourne le bon résultat d’action et affiche les données. Vous pouvez également tester si un contrôleur interagit avec les classes de modèle comme prévu.
Par exemple, listing 2 contient deux tests unitaires pour la méthode Create() du contrôleur de contacts. Le premier test unitaire vérifie que lorsqu’un contact valide est passé à la méthode Create(), la méthode Create() redirige vers l’action Index. En d’autres termes, lorsqu’un Contact valide est passé, la méthode Create() doit retourner un RedirectToRouteResult qui représente l’action Indexer.
Nous ne voulons pas tester la couche de service ContactManager lorsque nous testons la couche de contrôleur. Par conséquent, nous nous moquons de la couche de service avec le code suivant dans la méthode Initialize :
_service = New Mock(Of IContactManagerService)()
Dans le test unitaire CreateValidContact(), nous nous moquons du comportement de l’appel de la méthode CreateContact() de la couche de service avec la ligne de code suivante :
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(True)
Cette ligne de code amène le service ContactManager fictif à retourner la valeur true lorsque sa méthode CreateContact() est appelée. En se moquant de la couche de service, nous pouvons tester le comportement de notre contrôleur sans avoir à exécuter de code dans la couche de service.
Le deuxième test unitaire vérifie que l’action Create() renvoie la vue Créer lorsqu’un contact non valide est passé à la méthode. La méthode CreateContact() de la couche de service retourne la valeur false avec la ligne de code suivante :
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(False)
Si la méthode Create() se comporte comme prévu, elle doit retourner la vue Créer lorsque la couche de service retourne la valeur false. De cette façon, le contrôleur peut afficher les messages d’erreur de validation dans la vue Créer et l’utilisateur a la possibilité de corriger ces propriétés contact non valides.
Si vous envisagez de générer des tests unitaires pour vos contrôleurs, vous devez retourner des noms d’affichage explicites à partir de vos actions de contrôleur. Par exemple, ne retournez pas une vue comme celle-ci :
Return View()
Au lieu de cela, retournez la vue comme suit :
Return View(« Create »)
Si vous n’êtes pas explicite lors du renvoi d’une vue, la propriété ViewResult.ViewName renvoie une chaîne vide.
Listing 2 - Controllers\ContactControllerTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactControllerTest
Private _service As Mock(Of IContactManagerService)
<TestInitialize()> _
Public Sub Initialize()
_service = New Mock(Of IContactManagerService)()
End Sub
<TestMethod()> _
Public Sub CreateValidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(True)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), RedirectToRouteResult)
' Assert
Assert.AreEqual("Index", result.RouteValues("action"))
End Sub
<TestMethod()> _
Public Sub CreateInvalidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(False)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), ViewResult)
' Assert
Assert.AreEqual("Create", result.ViewName)
End Sub
End Class
Résumé
Dans cette itération, nous avons créé des tests unitaires pour notre application Gestionnaire de contacts. Nous pouvons exécuter ces tests unitaires à tout moment pour vérifier que notre application se comporte toujours comme prévu. Les tests unitaires servent de filet de sécurité pour notre application, ce qui nous permet de modifier notre application en toute sécurité à l’avenir.
Nous avons créé deux ensembles de tests unitaires. Tout d’abord, nous avons testé notre logique de validation en créant des tests unitaires pour notre couche de service. Ensuite, nous avons testé notre logique de contrôle de flux en créant des tests unitaires pour notre couche de contrôleur. Lors du test de notre couche de service, nous avons isolé nos tests pour notre couche de service de notre couche de dépôt en se moquant de notre couche de dépôt. Lors du test de la couche de contrôleur, nous avons isolé nos tests pour notre couche de contrôleur en se moquant de la couche de service.
Dans l’itération suivante, nous modifions l’application Gestionnaire de contacts afin qu’elle prenne en charge les groupes de contacts. Nous allons ajouter cette nouvelle fonctionnalité à notre application à l’aide d’un processus de conception logicielle appelé développement piloté par les tests.