Je recherche des conseils concernant les tests unitaires efficaces des contrôleurs mvc .NET.
Là où je travaille, beaucoup de ces tests utilisent moq pour se moquer de la couche de données et pour affirmer que certaines méthodes de la 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 retourné est correct. Je peux voir que cela apporte une certaine valeur, mais à lui seul, cela ne semble pas mériter l'effort d'écrire de nombreuses lignes de code moqueur (le modèle de données de notre application est très grand 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 unitaire du contrôleur doit tester les algorithmes de code dans vos méthodes d'action, pas dans votre couche de données. C'est une raison 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 un élément de l'application qui fournit ces circonstances au contrôleur/aux méthodes d'action. Il est utile d'affirmer qu'une méthode de service a été appelée par le contrôleur, car vous pouvez être certain que le contrôleur obtient les informations d'un autre endroit.
La vérification du type de modèle de vue renvoyé est utile car, si le type de modèle de vue incorrect est renvoyé, MVC lèvera une exception d'exécution. Vous pouvez empêcher cela de se produire en production en exécutant un test unitaire. Si le test échoue, la vue peut lever une exception en production.
Les tests unitaires peuvent être utiles car ils facilitent considérablement 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 réussissent.
Réponse au commentaire # 1
Si la modification de la mise en œuvre d'une méthode en cours de test nécessite la modification/suppression d'une méthode simulée de couche inférieure, le test unitaire doit également changer. Cependant, cela ne devrait pas se produire aussi souvent que vous le pensez.
Le workflow typique de refactorisation rouge-vert nécessite l'écriture de vos tests unitaires avant l'écriture des méthodes qu'ils testent. (Cela signifie que pendant un court laps de temps, votre code de test ne se compilera pas, et c'est pourquoi de nombreux développeurs jeunes/inexpérimentés ont du mal à adopter le refactor rouge vert.)
Si vous écrivez d'abord vos tests unitaires, vous arriverez à un point où vous savez que le contrôleur a besoin d'obtenir des informations d'une couche inférieure. Comment pouvez-vous être certain qu'il essaie d'obtenir ces informations? En se moquant de la méthode de couche inférieure qui fournit les informations et en affirmant que la méthode de couche inférieure est invoquée par le contrôleur.
J'ai peut-être mal parlé lorsque j'ai utilisé le terme "modification de la mise en œuvre". Quand la méthode d'action d'un contrôleur et le test unitaire correspondant doivent être modifiés pour changer ou supprimer une méthode simulée, vous changez vraiment le comportement du contrôleur. La refactorisation, par définition, signifie changer l'implémentation sans altérer le comportement global et les résultats attendus.
Red-green-refactor est une approche d'assurance qualité qui aide à prévenir les bugs et défauts de code avant qu'ils n'apparaissent. Les développeurs modifient généralement l'implémentation pour supprimer les bogues après leur apparition. Donc, pour réitérer, les cas qui vous inquiètent 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 amusez-vous unité les tester. S'ils sont gros et que vous avez rempli toute votre logique commerciale, je conviens que vous passerez votre vie à vous moquer de vos tests unitaires et à vous plaindre que c'est 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 doit être testée séparément.
Oui, vous devez tester jusqu'à la base de données. Le temps que vous consacrez à la simulation est moindre et la valeur que vous obtenez de la simulation est également très inférieure (80% des erreurs probables dans votre système ne peuvent pas être détectées par la simulation).
Lorsque vous testez tout le chemin d'un contrôleur à une base de données ou à un service Web, cela ne s'appelle pas un test unitaire mais un test d'intégration. Personnellement, je crois aux tests d'intégration par opposition aux tests unitaires (même s'ils ont tous deux des objectifs différents). Et je suis capable de réussir le développement piloté par les tests avec des tests d'intégration (test de scénario).
Voici comment cela fonctionne pour notre équipe. Chaque classe de test au début régénère la base de données et remplit/amorce les tables avec un ensemble minimal de données (par exemple: rôles d'utilisateur). Sur la base d'un besoin de contrôleurs, nous remplissons la base de données et vérifions si le contrôleur accomplit sa tâche. Ceci est conçu de telle manière que les données corrompues de la base de données laissées par d'autres méthodes n'échoueront jamais à un test. Excepté le temps nécessaire à l'exécution, à peu près toutes les qualités du test unitaire (même s'il s'agit d'une théorie) sont getting. Le temps nécessaire à l'exécution séquentielle peut être réduit avec les conteneurs. De plus, avec les conteneurs, nous n'avons pas besoin de recréer la base de données car chaque test obtient sa propre nouvelle base de données dans un conteneur (qui sera supprimé après le test).
Il n'y a eu que 2% de situations (ou très rarement) dans ma carrière où j'ai été obligé d'utiliser des maquettes/talons car il n'était pas possible de créer une source de données plus réaliste. Mais dans toutes les autres situations, des tests d'intégration étaient possibles.
Il nous a fallu du temps pour atteindre un niveau de maturité avec cette approche. nous avons un cadre de Nice qui traite de la population et de la récupération des données de test (citoyens de première classe). Et cela rapporte beaucoup de temps! La première étape consiste à dire adieu aux simulations 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.
===================================
Modifié après un commentaire ci-dessous: Démo
Le test d'intégration ou le test fonctionnel doit traiter directement avec DB/source. Pas de moqueries. Ce sont donc les étapes. Vous souhaitez tester getEmployee (emp_id) . toutes ces 5 étapes ci-dessous sont effectuées dans une seule méthode de test.
Maintenant Assert ()/Vérifiez si les données retournées sont correctes
Cela prouve que getEmployee () fonctionne. Les étapes jusqu'à 3 nécessitent que le code soit utilisé uniquement par le projet de test. L'étape 4 appelle le code d'application. Ce que je voulais dire, c'est créer un employé (étape 2) devrait être fait en testant le code du projet et non le code de l'application. S'il existe un code d'application pour créer un employé (par exemple: CreateEmployee () ), il ne doit pas être utilisé. De la même manière, lorsque nous testons CreateEmployee () puis GetEmployee () application le code ne doit pas être utilisé. Nous devrions avoir un code de projet de test pour récupérer les données d'une table.
De cette façon, il n'y a pas de moqueries! La raison de supprimer et de créer DB est d'empêcher DB d'avoir des données corrompues. Avec notre approche, le test réussira quel que soit le nombre de fois que nous l'exécutons.
Conseil spécial: à l'étape 5, getEmployee () renvoie un objet employé. Si un développeur supprime ou modifie ultérieurement un nom de champ, le test échoue. Et si un développeur ajoute un nouveau champ plus tard? Et il oublie d'ajouter un test pour cela (affirmer)? Le test ne le ramasserait pas. La solution consiste à ajouter une vérification du nombre de champs. Par exemple: l'objet employé a 4 champs (prénom, nom, désignation, sexe). Donc, Assert nombre de champs d'objet employé est 4. Donc, quand un nouveau champ est ajouté, notre test échouera en raison du nombre et rappelle au développeur d'ajouter un champ assert pour le champ nouvellement ajouté.
Et ceci est un excellent article sur les avantages de tests d'intégration par rapport aux tests unitaires parce que "les tests unitaires tuent!" (ça dit)
Le point 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 simulacres et affirmez le comportement de la méthode en vérifiant comment elle interagit avec les autres codes qui l'entourent - en vérifiant les méthodes externes qu'elle essaie d'appeler, mais en particulier en vérifiant la valeur qu'elle renvoie compte tenu des conditions.
Ainsi, dans le cas des méthodes Controller, qui renvoient ActionResults, il est très utile d'inspecter la valeur de ActionResult renvoyée.
Jetez un œil à la section 'Création de tests unitaires pour les contrôleurs' ici pour des exemples très clairs en utilisant Moq.
Voici un bel exemple de cette page qui teste qu'une vue appropriée est retournée lorsque le contrôleur tente de créer un enregistrement de contact et qu'il é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 d'intérêt à tester le contrôleur à l'unité, car ce n'est généralement qu'un morceau de code qui relie d'autres morceaux. Les tests unitaires incluent généralement beaucoup de moqueries et vérifient simplement que les autres services sont correctement connectés. Le test lui-même est le reflet du code d'implémentation.
Je préfère les tests d'intégration - je commence pas avec un contrôleur concret, mais avec une URL, et je vérifie que le modèle retourné a les bonnes valeurs. Avec l'aide de Ivonna , le test pourrait ressembler à:
var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);
var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);
Je peux simuler l'accès à la base de données, mais je préfère une approche différente: configurer une instance en mémoire de SQLite et la recréer à chaque nouveau test, avec les données requises. Cela rend mes tests assez rapides, mais au lieu de moqueries compliquées, je les clarifie, par ex. il suffit de créer et d'enregistrer une instance d'utilisateur, plutôt que de se moquer du UserService
(qui pourrait être un détail d'implémentation).
Habituellement, 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 des tests méthode par méthode et vous ne devriez même pas avoir à charger la vue ou le modèle, ce sont les parties que vous devez "simuler". Vous pouvez ensuite modifier les simulations pour renvoyer des valeurs ou des erreurs qui sont difficiles à reproduire dans d'autres tests.