Comment une unité doit-elle tester un contrôleur .NET MVC?

Je cherche des conseils concernant les tests unitaires efficaces des contrôleurs .NET mvc.

Là où je travaille, beaucoup de ces tests utilisent moq pour se moquer de la couche de données et affirmer que certaines méthodes de couche de données sont appelées. Cela ne me semble pas utile, car cela vérifie essentiellement que l’implémentation n’a pas changé plutôt que de tester l’API.

J’ai également lu des articles recommandant des choses comme vérifier que le type de modèle de vue renvoyé est correct. Je peux voir que cela apporte une certaine valeur, mais seul cela ne semble pas mériter l’effort d’écrire plusieurs lignes de code moqueur (le modèle de données de notre application est très vaste et complexe).

Quelqu’un peut-il suggérer de meilleures approches pour tester les unités de contrôleur ou expliquer pourquoi les approches ci-dessus sont valides / utiles?

Merci!

Un test d’unité de contrôleur devrait tester les algorithmes de code dans vos méthodes d’action, pas dans votre couche de données. C’est l’une des raisons de se moquer de ces services de données. Le contrôleur s’attend à recevoir certaines valeurs des référentiels / services / etc, et à agir différemment lorsqu’il reçoit des informations différentes de leur part.

Vous écrivez des tests unitaires pour affirmer que le contrôleur se comporte de manière très spécifique dans des scénarios / circonstances très spécifiques. Votre couche de données est une partie de l’application qui fournit ces circonstances aux méthodes de contrôleur / action. L’affirmation qu’une méthode de service a été appelée par le contrôleur est précieuse car vous pouvez être certain que le contrôleur obtient les informations depuis un autre endroit.

La vérification du type de viewmodel renvoyé est précieuse car, si le type de viewmodel incorrect est renvoyé, MVC lancera une exception d’exécution. Vous pouvez empêcher cela en production en exécutant un test unitaire. Si le test échoue, la vue peut générer une exception en production.

Les tests unitaires peuvent être précieux car ils facilitent grandement la refactorisation. Vous pouvez modifier l’implémentation et affirmer que le comportement est toujours le même en vous assurant que tous les tests unitaires sont réussis.

Réponse au commentaire # 1

Si la modification de l’implémentation d’une méthode sous test appelle la modification / suppression d’une méthode simulée de couche inférieure, le test unitaire doit également être modifié. Cependant, cela ne devrait pas arriver aussi souvent que vous le pensez.

Le stream de travaux typique de red-green-refactor nécessite l’écriture de vos tests unitaires avant d’ écrire les méthodes qu’ils testent. (Cela signifie que pour un court laps de temps, votre code de test ne sera pas compilé, et c’est pourquoi de nombreux développeurs jeunes / inexpérimentés ont des difficultés à adopter le refactor green green.)

Si vous écrivez d’abord vos tests unitaires, vous en arrivez à un point où vous savez que le contrôleur doit obtenir des informations d’une couche inférieure. Comment pouvez-vous être certain qu’il essaie d’obtenir cette information? En masquant la méthode de couche inférieure qui fournit les informations et en affirmant que la méthode de couche inférieure est appelée par le contrôleur.

Je me suis peut-être trompé lorsque j’ai utilisé le terme «modification de la mise en œuvre». Lorsque la méthode d’action d’un contrôleur et le test d’unité correspondant doivent être modifiés pour modifier ou supprimer une méthode simulée, vous modifiez réellement le comportement du contrôleur. Refactoring, par définition, signifie modifier la mise en œuvre sans modifier le comportement global et les résultats attendus.

Red-green-refactor est une approche d’assurance de la qualité qui aide à prévenir les bogues et les défauts de code avant qu’ils apparaissent. Généralement, les développeurs changent d’implémentation pour supprimer les bogues après leur apparition. Donc, pour répéter, les cas qui vous préoccupent ne devraient pas se produire aussi souvent que vous le pensez.

Vous devez d’abord mettre vos contrôleurs au régime. Ensuite, vous pouvez avoir des unités amusantes pour les tester. Si elles sont grosses et que vous avez rempli toute la logique de votre entreprise, je conviens que vous passerez votre vie à vous moquer de vos tests unitaires et à vous plaindre qu’il s’agit d’une perte de temps.

Lorsque vous parlez de logique complexe, cela ne signifie pas nécessairement que cette logique ne peut pas être séparée en différentes couches et que chaque méthode soit testée de manière isolée.

Le but d’un test unitaire est de tester le comportement d’une méthode de manière isolée, sur la base d’un ensemble de conditions. Vous définissez les conditions du test à l’aide de mocks et vérifiez le comportement de la méthode en vérifiant son interaction avec les autres codes, en vérifiant quelles méthodes externes il essaie d’appeler, mais surtout en vérifiant la valeur renvoyée.

Ainsi, dans le cas des méthodes Controller, qui renvoient ActionResults, il est très utile d’inspecter la valeur du ActionResult renvoyé.

Jetez un coup d’oeil à la section «Créer des tests unitaires pour les contrôleurs» ici pour quelques exemples très clairs utilisant Moq.

Voici un exemple intéressant de cette page qui vérifie qu’une vue appropriée est renvoyée lorsque le contrôleur tente de créer un enregistrement de contact et que celui-ci échoue.

[TestMethod] public void CreateInvalidContact() { // Arrange var contact = new Contact(); _service.Expect(s => s.CreateContact(contact)).Returns(false); var controller = new ContactController(_service.Object); // Act var result = (ViewResult)controller.Create(contact); // Assert Assert.AreEqual("Create", result.ViewName); } 

Je ne vois pas beaucoup de points dans les tests unitaires du contrôleur, car il ne s’agit généralement que d’un morceau de code qui relie d’autres pièces. Le test unitaire comprend généralement beaucoup de moqueries et vérifie simplement que les autres services sont correctement connectés. Le test lui-même reflète le code d’implémentation.

Je préfère les tests d’intégration – Je ne commence pas avec un contrôleur concret, mais avec une URL et vérifie que le modèle renvoyé a les valeurs correctes. Avec l’aide d’ Ivonna , le test pourrait ressembler à ceci :

 var response = new TestSession().Get("/Users/List"); Assert.IsInstanceOf(response.Model); var model = (UserListModel) response.Model; Assert.AreEqual(1, model.Users.Count); 

Je peux simuler l’access à la firebase database, mais je préfère une approche différente: configurer une instance de SQLite en mémoire et la recréer avec chaque nouveau test, avec les données requirejses. Cela rend mes tests assez rapides, mais au lieu de me moquer compliqué, je les rends clairs, par exemple, simplement créer et enregistrer une instance d’utilisateur, plutôt que de UserService le UserService (qui pourrait être un détail d’implémentation).

Oui, vous devriez tester jusqu’au DB. Le temps que vous mettez dans la moquerie est moindre et la valeur que vous obtenez de la moquerie est également moindre (80% des erreurs probables dans votre système ne peuvent être détectées par moquerie).

Lorsque vous testez tout du contrôleur à la firebase database ou au service Web, il ne s’agit pas de test unitaire mais de test d’intégration. Je crois personnellement aux tests d’intégration plutôt qu’aux tests unitaires. Et je suis capable de faire du développement piloté avec succès.

Voici comment cela fonctionne pour notre équipe. Chaque classe de test au début régénère la firebase database et remplit / génère les tables avec un ensemble minimum de données (ex: rôles d’utilisateur). Sur la base des contrôleurs, nous devons renseigner la firebase database et vérifier si le contrôleur effectue sa tâche. Ceci est conçu de telle manière que les données corrompues de la firebase database laissées par d’autres méthodes n’échoueront jamais un test. Sauf le temps nécessaire pour exécuter, à peu près toutes les qualités de test unitaire (même si c’est une théorie) sont inoubliables.

Il n’y avait que 2% de situations (ou très rarement) dans ma carrière où j’ai été obligé d’utiliser des simulacres, car il n’était pas possible de créer une source de données plus réaliste. Mais dans toutes les autres situations, les tests d’intégration étaient une possibilité.

Il nous a fallu du temps pour atteindre un niveau de maturité avec cette approche. nous avons un cadre agréable qui traite de la population de données de test et de la récupération (citoyens de première classe). Et ça paye beaucoup de temps :). La première étape consiste à dire adieu aux simulacres et aux tests unitaires. Si les moqueries n’ont pas de sens, elles ne sont pas pour vous! Le test d’intégration vous permet de bien dormir

===================================

Edité après un commentaire ci-dessous: Demo

Le test d’intégration ou test fonctionnel doit traiter directement la firebase database. Pas de simulacre. Donc ce sont les étapes. Vous voulez tester getEmployee () . Toutes les 5 étapes ci-dessous sont effectuées en une seule méthode de test.

  1. Drop DB
  2. Créer une firebase database et remplir des rôles et d’autres données infra
  3. Créer un enregistrement d’employé avec ID
  4. Utilisez cet identifiant et appelez getEmployee ()
  5. Maintenant Assert () / Vérifiez si les données renvoyées sont correctes

    Cela prouve que getEmployee () fonctionne. Les étapes jusqu’à 3 exigent que le code soit uniquement utilisé par le projet de test. L’étape 4 appelle le code de l’application. Ce que je voulais dire, c’est créer un employé (étape 2) devrait être fait par le code du projet de test et non par le code de l’application. S’il existe un code d’application pour créer un employé (par exemple: CreateEmployee () ), cela ne doit pas être utilisé. De même, lorsque nous testons CreateEmployee ( ) , le code de l’application GetEmployee () ne doit pas être utilisé. Nous devrions avoir un code de projet de test pour extraire des données d’une table.

De cette façon, il n’y a pas de simulacre! La raison pour supprimer et créer la firebase database est d’empêcher la firebase database d’avoir des données erronées. Avec notre approche, le test passera combien de fois nous l’exécuterons.

Conseil spécial: À l’étape 5, si getEmployee () renvoie un object employé. Si un développeur ultérieur supprime ou modifie un nom de champ, le test est interrompu car les champs sont vérifiés. Et si un développeur ajoute un nouveau champ plus tard? Et il / elle oublie d’append un test (assert)? La solution consiste à toujours append une vérification du nombre de champs. Par exemple: L’object employé a 4 champs (Prénom, Nom, Désignation, Sexe). Le nombre de champs Assert d’object employé est donc 4. Et notre test échouera à cause du nombre et rappelle au développeur d’append un champ d’affirmation pour le champ nouvellement ajouté. De plus, notre code de test appenda ce nouveau champ à la firebase database et le récupérera et le vérifiera.

Et c’est un excellent article sur les avantages des tests d’intégration par rapport aux tests unitaires, car “les tests unitaires tuent!” (ça dit)

Généralement, lorsque vous parlez de tests unitaires, vous testez une procédure ou une méthode individuelle, pas un système entier, tout en essayant d’éliminer toutes les dépendances externes.

En d’autres termes, lorsque vous testez le contrôleur, vous écrivez la méthode des tests par méthode et vous ne devriez même pas avoir la vue ou le modèle chargé, ce sont les parties que vous devez “simuler”. Vous pouvez ensuite modifier les parameters simulés pour renvoyer des valeurs ou des erreurs difficiles à reproduire dans d’autres tests.