Tests unitaires des contrôleurs dans ASP.NET Web API 2

Cette rubrique décrit certaines techniques spécifiques pour les contrôleurs de test unitaire dans l’API Web 2. Avant de lire cette rubrique, vous pouvez lire le didacticiel Test unitaire API Web ASP.NET 2, qui montre comment ajouter un projet de test unitaire à votre solution.

Versions logicielles utilisées dans le tutoriel

Notes

J’ai utilisé Moq, mais la même idée s’applique à n’importe quel framework de simulation. Moq 4.5.30 (et versions ultérieures) prend en charge Visual Studio 2017, Roslyn et .NET 4.5 et versions ultérieures.

Un modèle courant dans les tests unitaires est « arrange-act-assert » :

  • Organiser : configurez les conditions préalables à l’exécution du test.
  • Agir : effectuez le test.
  • Assert : vérifiez que le test a réussi.

Dans l’étape organiser, vous utiliserez souvent des objets fictifs ou stub. Cela réduit le nombre de dépendances. Le test est donc axé sur le test d’une chose.

Voici quelques éléments que vous devez tester unitairement dans vos contrôleurs d’API web :

  • L’action retourne le type de réponse correct.
  • Les paramètres non valides retournent la réponse d’erreur correcte.
  • L’action appelle la méthode correcte sur le dépôt ou la couche de service.
  • Si la réponse inclut un modèle de domaine, vérifiez le type de modèle.

Ce sont quelques-uns des éléments généraux à tester, mais les spécificités dépendent de l’implémentation de votre contrôleur. En particulier, cela fait une grande différence si vos actions de contrôleur retournent HttpResponseMessage ou IHttpActionResult. Pour plus d’informations sur ces types de résultats, consultez Résultats de l’action dans l’API web 2.

Actions de test qui retournent HttpResponseMessage

Voici un exemple de contrôleur dont les actions retournent HttpResponseMessage.

public class ProductsController : ApiController
{
    IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    public HttpResponseMessage Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(product);
    }

    public HttpResponseMessage Post(Product product)
    {
        _repository.Add(product);

        var response = Request.CreateResponse(HttpStatusCode.Created, product);
        string uri = Url.Link("DefaultApi", new { id = product.Id });
        response.Headers.Location = new Uri(uri);

        return response;
    }
}

Notez que le contrôleur utilise l’injection de dépendances pour injecter un IProductRepository. Cela rend le contrôleur plus testable, car vous pouvez injecter un dépôt fictif. Le test unitaire suivant vérifie que la Get méthode écrit un Product dans le corps de la réponse. Supposons que repository soit un fictive IProductRepository.

[TestMethod]
public void GetReturnsProduct()
{
    // Arrange
    var controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    // Act
    var response = controller.Get(10);

    // Assert
    Product product;
    Assert.IsTrue(response.TryGetContentValue<Product>(out product));
    Assert.AreEqual(10, product.Id);
}

Il est important de définir La requête et la configuration sur le contrôleur. Sinon, le test échoue avec une exception ArgumentNullException ou InvalidOperationException.

La Post méthode appelle UrlHelper.Link pour créer des liens dans la réponse. Cela nécessite un peu plus de configuration dans le test unitaire :

[TestMethod]
public void PostSetsLocationHeader()
{
    // Arrange
    ProductsController controller = new ProductsController(repository);

    controller.Request = new HttpRequestMessage { 
        RequestUri = new Uri("http://localhost/api/products") 
    };
    controller.Configuration = new HttpConfiguration();
    controller.Configuration.Routes.MapHttpRoute(
        name: "DefaultApi", 
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional });

    controller.RequestContext.RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary { { "controller", "products" } });

    // Act
    Product product = new Product() { Id = 42, Name = "Product1" };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual("http://localhost/api/products/42", response.Headers.Location.AbsoluteUri);
}

La classe UrlHelper a besoin de l’URL de requête et des données de routage. Le test doit donc définir des valeurs pour ceux-ci. Une autre option est UrlHelper fictif ou stub. Avec cette approche, vous remplacez la valeur par défaut d’ApiController.Url par une version fictive ou stub qui retourne une valeur fixe.

Réécritons le test à l’aide de l’infrastructure Moq . Installez le Moq package NuGet dans le projet de test.

[TestMethod]
public void PostSetsLocationHeader_MockVersion()
{
    // This version uses a mock UrlHelper.

    // Arrange
    ProductsController controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    string locationUrl = "http://location/";

    // Create the mock and set up the Link method, which is used to create the Location header.
    // The mock version returns a fixed string.
    var mockUrlHelper = new Mock<UrlHelper>();
    mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
    controller.Url = mockUrlHelper.Object;

    // Act
    Product product = new Product() { Id = 42 };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}

Dans cette version, vous n’avez pas besoin de configurer de données de routage, car l’UrlHelper fictif retourne une chaîne constante.

Actions de test qui retournent IHttpActionResult

Dans l’API Web 2, une action de contrôleur peut retourner IHttpActionResult, qui est analogue à ActionResult dans ASP.NET MVC. L’interface IHttpActionResult définit un modèle de commande pour la création de réponses HTTP. Au lieu de créer la réponse directement, le contrôleur retourne un IHttpActionResult. Plus tard, le pipeline appelle IHttpActionResult pour créer la réponse. Cette approche facilite l’écriture de tests unitaires, car vous pouvez ignorer une grande partie de la configuration nécessaire pour HttpResponseMessage.

Voici un exemple de contrôleur dont les actions retournent IHttpActionResult.

public class Products2Controller : ApiController
{
    IProductRepository _repository;

    public Products2Controller(IProductRepository repository)
    {
        _repository = repository;
    }

    public IHttpActionResult Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }

    public IHttpActionResult Post(Product product)
    {
        _repository.Add(product);
        return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
    }

    public IHttpActionResult Delete(int id)
    {
        _repository.Delete(id);
        return Ok();
    }

    public IHttpActionResult Put(Product product)
    {
        // Do some work (not shown).
        return Content(HttpStatusCode.Accepted, product);
    }    
}

Cet exemple montre certains modèles courants utilisant IHttpActionResult. Voyons comment les tester à l’unité.

L’action retourne 200 (OK) avec un corps de réponse

La Get méthode appelle Ok(product) si le produit est trouvé. Dans le test unitaire, vérifiez que le type de retour est OkNegotiatedContentResult et que le produit retourné a l’ID approprié.

[TestMethod]
public void GetReturnsProductWithSameId()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    mockRepository.Setup(x => x.GetById(42))
        .Returns(new Product { Id = 42 });

    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(42);
    var contentResult = actionResult as OkNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(42, contentResult.Content.Id);
}

Notez que le test unitaire n’exécute pas le résultat de l’action. Vous pouvez supposer que le résultat de l’action crée la réponse HTTP correctement. (C’est pourquoi l’infrastructure d’API web a ses propres tests unitaires !)

L’action retourne 404 (introuvable)

La Get méthode appelle NotFound() si le produit est introuvable. Dans ce cas, le test unitaire vérifie simplement si le type de retour est NotFoundResult.

[TestMethod]
public void GetReturnsNotFound()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
}

L’action retourne 200 (OK) sans corps de réponse

La Delete méthode appelle Ok() pour retourner une réponse HTTP 200 vide. Comme dans l’exemple précédent, le test unitaire vérifie le type de retour, en l’occurrence OkResult.

[TestMethod]
public void DeleteReturnsOk()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Delete(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(OkResult));
}

L’action retourne 201 (créé) avec un en-tête Location

La Post méthode appelle CreatedAtRoute pour renvoyer une réponse HTTP 201 avec un URI dans l’en-tête Location. Dans le test unitaire, vérifiez que l’action définit les valeurs de routage correctes.

[TestMethod]
public void PostMethodSetsLocationHeader()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Post(new Product { Id = 10, Name = "Product1" });
    var createdResult = actionResult as CreatedAtRouteNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(createdResult);
    Assert.AreEqual("DefaultApi", createdResult.RouteName);
    Assert.AreEqual(10, createdResult.RouteValues["id"]);
}

L’action retourne un autre 2xx avec un corps de réponse

La Put méthode appelle Content pour renvoyer une réponse HTTP 202 (acceptée) avec un corps de réponse. Ce cas est similaire au retour de 200 (OK), mais le test unitaire doit également case activée le code status.

[TestMethod]
public void PutReturnsContentResult()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Put(new Product { Id = 10, Name = "Product" });
    var contentResult = actionResult as NegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.AreEqual(HttpStatusCode.Accepted, contentResult.StatusCode);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(10, contentResult.Content.Id);
}

Ressources supplémentaires