Architecture Web MVC - Tests unitaires des contrôleurs et des services

Architecture Web MVC - Tests unitaires des contrôleurs et des services - Divers - Programmation

Marsh Posté le 15-04-2019 à 14:21:15    

Bonjour tout le monde :)  
 
Je commence actuellement à développer une application web avec un framework de type MVC (ASP.NET MVC 5 pour mon cas précis mais ça ne change pas ma question).
 
Je cherche à écrire correctement mes tests unitaires sur mes différentes couches.
 
Voici les différentes couches:
- Data access qui contient le contexte (fourni par un ORM) pour accéder à ma base de données
- Domain qui contient mes diverses classes de services, services qui font appel au contexte pour récupérer les objets dans la DB
- Les contrôleurs qui appellent les services de la couche précédente, puis envoient le modèle aux vues  
 
Je cherche à donc faire des tests unitaires sur les 2 dernières couches: les services et les contrôleurs. Je teste assez simplement les services en utilisant un framework de mocking qui mock entièrement le contexte fourni par mon ORM (pour info c'est Effort pour Entity Framework).
 
En revanche pour tester les contrôleurs, j'ai deux choix:
1) mocker directement le service
2) utiliser le service réel mais avec le contexte de la DB lui mocké par contre
 
Avec 1), ça me permet de bien isoler le contrôleur de la couche inférieure, aucune dépendance au service. Si mon test sur le contrôleur échoue, c'est bien à cause du code contenu dans le contrôleur. En revanche, ça nécessite que je crée un mock du service, c'est du code supplémentaire à maintenir.
 
Avec 2), je me retrouve à tester à la fois le contrôleur et le service. Si mon test sur le contrôleur échoue, ça peut être à cause du code contenu dans le service. En revanche, l'écriture du test est facilitée car je réutilise le mock du contexte de la DB.
 
Ma question est, est-ce que le 2e choix est vraiment mauvais ? Techniquement, le code contenu dans mon service est déjà couvert par les tests unitaires du service. Donc si le test du contrôleur échoue et que ceux du service passent, le problème viendrait du code dans le contrôleur. Ça me parait plus simple de ne mocker qu'un seul objet (ici le contexte de la DB) plutôt que de tout mocker.  
 
Qu'est-ce que vous feriez à ma place ?
 
Merci d'avance pour la lecture et pour d'éventuelles réponses :jap:


Message édité par fugacef le 15-04-2019 à 14:22:15
Reply

Marsh Posté le 15-04-2019 à 14:21:15   

Reply

Marsh Posté le 15-04-2019 à 18:56:57    

Je ne sais pas quelles sont les pratiques "académiques" mais perso, confronté à la même question, je choisis toujours de mocker le moins de trucs possibles et de tester les éléments à chaque niveau où ils apparaissent.
Donc effectivement mon contrôleur va généralement consommer le vrai service même dans les tests, et si le test échoue j'irai voir si c'est à cause du service ou du contrôleur en me référant au test du service lui-même.  
Pour le peu de temps en plus que ça prendra, je préfère largement ça que maintenir une jungle de mocks et en plus risquer d'avoir une couverture de code moindre.


---------------
Réalisation amplis classe D / T      Topic .Net - C# @ Prog
Reply

Marsh Posté le 15-04-2019 à 21:22:39    

Si on parle de tests unitaires, alors je pense tres fortement qu'il faut mocker les services quand on teste les controllers.

 

1- Le test unitaire n'est pas cense fournir de la couverture de test uniquement, mais aussi permettre de garder un code modulaire, et servir de documentation au code car les tests documentent la maniere dont doit se comporter le code.
Ce dernier point implique que les tests doivent tester la responsabilite de la classe testee, en isolation des autres classes, car sinon on ne teste plus une classe mais un ensemble de classes, et le test n'est plus unitaire.

 

2- Les tests, comme le code, doivent satisfaire aux exigences de maintenabilite du projet. Or, si je ne mocke pas mon service et que, plus tard, je fais evoluer l'API de mon service, j'aurai plussieurs classes de tests qui vont echouer. Cela implique que je vais devoir corriger plusieurs fois le test du meme comportement, et que j'aurai donc de la duplication de code dans mon test, ce qui est une mauvaise pratique au meme titre que dans mon code.
En outre pour etre exhaustif, je vais devoir tester tous les chemins de mon controlleur * tous les chemins de mon service, et avoir donc un produit cartesien du nombre de methodes de tests a maintenir plus tard.

 

3- Comme le reste du code, un test unitaire doit etre clair, et autant il est relativement simple de creer une methode pour expliquer que l'on teste un comportement:

 
Code :
  1. public void shouldReturnNoResultWhenGivenBadName(){
  2.   var result =  controlleur.getPerson ("not existing name" );
  3.   assertThat(result).isNull()
  4. }
 

Autant ca devient dur d'etre clair quand on teste deux classes avec chacune un etat:

 


Code :
  1. public void shouldReturnNoResultWhenGivenBadNameAndServiceReturnsNothingWhenGivenABadName(){
  2.   var result =  controlleur.getPerson ("not existing name" );
  3.   assertThat(result).isNull()
  4. }
 


Code :
  1. public void shouldReturnNoResultWhenGivenGoodNameAndServiceReturnsNothingWhenGivenABadName(){
  2.   var result =  controlleur.getPerson ("Existing person" );
  3.   assertThat(result).isNull()
  4. }
 

Je pense que la difference entre ces deux tests est compliquee a comprendre et a lire.

 

Je voudrais aussi dire qu'il ne faut pas avoir de scrupules a tester uniquement ce que fait la methode du controller, meme si elle ne fait pas grand chose, meme si elle ne fait que "passe-plat", car c'est sa responsabilite.

 

Je connais mal .NET, mais je doute que le cout de creation des mocks soit un vrai frein.

 

Par ex. je viens de trouver ca, qui me parait simple:

 
Code :
  1. var mockUserService = new Mock<IUserService>();
 

Une fois fait et parametre, le mock du service est mutualisable entre toutes les classes de test qui utilisent le service, et si je me refere a mon experience java, c'est pas vraiment un frein.


Message édité par gelatine_velue le 15-04-2019 à 21:28:31
Reply

Marsh Posté le 16-04-2019 à 09:29:22    

Perso, j'aurais une préférence pour la solution 2 mais en prenant soin de bien ordonnancer les tests. D'abord on teste le service puis on teste le contrôleur. Ainsi, si le test du service est passé OK et que celui du contrôleur est NOK, alors on peut en déduire avec une bonne proba que c'est le code du contrôleur qui a un pb.
Eventuellement, une fois tous les tests passés, tu peux relancer les tests sur les contrôleurs qui ont été NOK. S'ils sont OK, alors c'est qu'il s'est peut être passé un événement extérieur qui a perturbé les tests. S'ils sont à nouveau NOK, il n'y a plus trop de doute à avoir : c'est le code du contrôleur qui est en cause.


---------------
Astres, outil de help-desk GPL : http://sourceforge.net/projects/astres, ICARE, gestion de conf : http://sourceforge.net/projects/icare, Outil Planeta Calandreta : https://framalibre.org/content/planeta-calandreta
Reply

Marsh Posté le 16-04-2019 à 12:47:23    

Merci à tous pour vos réponses intéressantes, je commence à mieux comprendre. J'ai bien réfléchi sur mon cas, et je vais partir sur l'option 1 finalement. Je vous explique pourquoi plus en détail.
 
Concrètement, mon projet c'est: la DB c'est une table Users et une table Projets, et il y a une système d'authentification via la table Users. Y a une relation 1-to-many entre les deux. Un User possède de 0 à n Projets, un Projet n'appartient qu'à un seul User, sans User un Projet n'existe pas.
 
J'ai un contrôleur Projet, qui gère les opérations CRUD sur la table Projet (des action methodes Index, Details, Create, Edit, Delete). Bien évidemment, ces opérations ne sont possibles que pour une personne loguée, et les droits lecture/écriture ne sont fournis que pour les Projets qui appartiennent à ce dit User.
Je vous montre le code de l'action method "Details", qui vise à afficher les détails d'un Projet spécifique. Cette méthode est appelée en faisait un GET sur l'URL suivante: localhost/MonProjet/MonControleur/Details/<id>
 

Code :
  1. public async Task<ActionResult> Details(int? id)
  2. {
  3.     if (id == null)
  4.     {
  5.         // Aucun id fourni
  6.         return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  7.     }
  8.     Project project= await this.Service.FindProject(id);
  9.    
  10.     if (project== null)
  11.     {
  12.         // Id fourni mais ne correspondant à aucun Projet existant dans la DB
  13.         return HttpNotFound();
  14.     }
  15.    
  16.     if (project.UserID != this.UserId)
  17.     {
  18.         // Id fourni correspondant à un Projet existant en DB, mais n'appartenant pas à l'utilisateur connecté
  19.         // On redirige sur la page d'accueil
  20.         return RedirectToAction("Index" );
  21.     }
  22.     // Id fourni correspondant à un Projet existant en DB et appartenant à l'utilisateur connecté
  23.     return View(project);
  24. }


this.Service et this.UserID (id de l'utilisateur connecté) sont injectés par ailleurs dans le contrôleur. On voit qu'il y a 4 cas de figure suivant l'id fourni comme paramètre dans l'URL.
 
Pour tester ces méthodes, je dois écrire 4 tests. On en revient à mes deux solutions:
1) Mocker le Service, le contexte du DAL n'existe plus
Je mock le Service, je stub la méthode Service.FindProject et je lui fais retourner l'objet Project qui va bien suivant le cas que je teste.
Avantages :
- Aucune référence au contexte du DAL, tout changement à ce niveau-là ne fera pas l'objet d'une retouche du test du contrôleur
- Rapide à exécuter (car le framework de mocking du contexte DB est lourd)
Inconvénients :
- Complique lorsque y a des changements dans la claasse Service. Si finalement je décide que la méthode FindProject fonctionne autrement, ou qu'elle n'existe plus, je dois aussi modifier le test. Mais est-ce que ça semble aberrant ? Si la méthode FindProject n'existe plus, je dois modifier le code du contrôleur de toute manière.
 
2) Utiliser le Service réel, mocker le contexte du DAL
J'utilise le Service réel, je mocke le contexte du DAL avec mon framework spécifique
Avantages :
- Je peux modifier sans problème ma classe Service. Si je décide que FindProject fonctionne autrement (par exemple retourne un object Projet générique et non plus null si il n'y a aucune entrée correspondante en DB), ou même si je décide de supprimer la méthode FindProject, je n'ai pas à modifier mon test du contrôleur. A noter que ceci n'a un gros intérêt que si la classe Service n'est pas testée (exemple classe privée), sinon il faut quand même modifier le test du Service.
Inconvénients :
- Le setup du test est lourd, je dois mocker mon contexte, le remplir avec les données qui vont bien. Dans mon cas précis, le framework de mocking pour le contexte est quand même assez lourd, il faut compter 3-4 secondes pour l'initialisation pour chacune des classes de test l'utilisant.
- Si j'ai une autre implémentation de ma DAL, je dois également modifier le test du contrôleur (en plus du test du service).
 
 
En y réfléchissant mieux, je trouve la 1) meilleure. Ce qui me gênait avec la 1), c'est que si je décidais de modifier le comportement de mon service, je devais aussi modifier le test de mon contrôleur vu qu'il fait appel au service. Mais finalement, c'est assez logique vu que si le changement est impactant tel qu'il modifie le comportement du contrôleur, toucher au test associé à du sens. Et aussi, plus j'écrivais du code, et puis je voyais que le setup du framework de mocking était lourd quand même.  

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed