web-dev-qa-db-fra.com

Existe-t-il une réelle valeur dans le test unitaire d'un contrôleur dans ASP.NET MVC?

J'espère que cette question donne des réponses intéressantes parce que c'est celle qui m'a dérangé pendant un certain temps.

Existe-t-il une valeur réelle dans le test unitaire d'un contrôleur dans ASP.NET MVC?

Ce que je veux dire par là, c'est que la plupart du temps (et je ne suis pas un génie), mes méthodes de contrôleur sont, même à leur plus complexe quelque chose comme ça:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

La plupart des tâches lourdes sont effectuées par le pipeline MVC ou ma bibliothèque de services.

Alors peut-être que les questions à poser pourraient être:

  • quelle serait la valeur des tests unitaires de cette méthode?
  • cela ne se briserait-il pas sur Request.UserHostAddress et ModelState avec une NullReferenceException? Dois-je essayer de les moquer?
  • si je réfracte cette méthode en un "assistant" réutilisable (ce que je devrais probablement, compte tenu du nombre de fois que je le fais!), des tests qui en valent la peine alors que tout ce que je teste sont principalement le "pipeline" qui, vraisemblablement, a été testé à moins d'un pouce de sa vie par Microsoft?

Je pense que mon point vraiment est que faire ce qui suit semble tout à fait inutile et faux

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Évidemment, je suis obtus avec cet exemple exagérément inutile, mais quelqu'un a-t-il la sagesse d'ajouter ici?

Au plaisir ... Merci.

33
LiverpoolsNumber9

Même pour quelque chose d'aussi simple, un test unitaire aura plusieurs objectifs

  1. Confiance, ce qui a été écrit est conforme à la sortie attendue. Il peut sembler trivial de vérifier qu'il renvoie la vue correcte, mais le résultat est une preuve objective que l'exigence a été remplie
  2. Les tests de régression. Si la méthode Create doit changer, vous avez toujours un test unitaire pour la sortie attendue. Oui, la sortie peut changer en même temps et cela se traduit par un test fragile, mais il s'agit toujours d'une vérification par rapport au contrôle des modifications non géré

Pour cette action particulière, je testerais les éléments suivants

  1. Que se passe-t-il si _myService est nul?
  2. Que se passe-t-il si _myService.Create lève une exception, en lance-t-il des spécifiques à gérer?
  3. Un _myService.Create réussi renvoie-t-il la vue _Success?
  4. Les erreurs se propagent-elles jusqu'à ModelState?

Vous avez souligné la vérification de Request and Model pour NullReferenceException et je pense que ModelState.IsValid se chargera de gérer NullReference for Model.

Se moquer de la demande vous permet de vous prémunir contre une demande nulle qui est généralement impossible en production, je pense, mais peut se produire dans un test unitaire. Dans un test d'intégration, il vous permettrait de fournir différentes valeurs UserHostAddress (une demande est toujours entrée par l'utilisateur en ce qui concerne le contrôle et doit être testée en conséquence)

18
Kevin

Mes contrôleurs sont également très petits. La plupart de la "logique" des contrôleurs est gérée à l'aide d'attributs de filtre (intégrés et manuscrits). Donc, mon contrôleur n'a généralement qu'une poignée d'emplois:

  • Créez des modèles à partir de chaînes de requête HTTP, de valeurs de formulaire, etc.
  • Effectuer une validation de base
  • Appeler dans mes données ou ma couche métier
  • Générez un ActionResult

La plupart des liaisons de modèle sont effectuées automatiquement par ASP.NET MVC. DataAnnotations gère également la majeure partie de la validation.

Même avec si peu de choses à tester, je les écris généralement. Fondamentalement, je teste que mes référentiels sont appelés et que le type ActionResult correct est renvoyé. J'ai une méthode pratique pour ViewResult pour m'assurer que le bon chemin de vue est retourné et que le modèle de vue ressemble à ce que j'attends. J'en ai un autre pour vérifier que le bon contrôleur/action est défini pour RedirectToActionResult. J'ai d'autres tests pour JsonResult, etc. etc.

Un résultat malheureux de la sous-classification de la classe Controller est qu'elle fournit de nombreuses méthodes pratiques qui utilisent HttpContext en interne. Cela rend difficile le test unitaire du contrôleur. Pour cette raison, je place généralement des appels dépendants de HttpContext derrière une interface et je transmets cette interface au constructeur du contrôleur (j'utilise l'extension Web Ninject pour créer mes contrôleurs pour moi). Cette interface est généralement l'endroit où je colle les propriétés d'assistance pour accéder à la session, aux paramètres de configuration, aux assistants IPrinciple et URL.

Cela demande beaucoup de diligence raisonnable, mais je pense que cela en vaut la peine.

3
Travis Parks

Évidemment, certains contrôleurs sont beaucoup plus complexes que cela, mais basés uniquement sur votre exemple:

Que se passe-t-il si myService lève une exception?

En remarque.

En outre, je remettrais en question la sagesse de transmettre une liste par référence (ce n'est pas nécessaire car c # passe par référence de toute façon mais même si ce n'était pas le cas) - passer une action errorAction (Action) que le service peut ensuite utiliser pour pomper des messages d'erreur vers qui pourrait ensuite être gérée comme vous le souhaitez (peut-être que vous voulez l'ajouter à la liste, peut-être que vous voulez ajouter une erreur de modèle, peut-être que vous voulez l'enregistrer).

Dans votre exemple:

au lieu d'erreurs ref, faites (chaîne s) => ModelState.AddModelError ("", s) par exemple.

2
Michael