Partager via


Contrôleurs de test unitaire dans ASP.NET API web 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 Unit Testing ASP.NET l’API web 2, qui montre comment ajouter un projet de test unitaire à votre solution.

Versions logicielles utilisées dans le didacticiel

Note

J’ai utilisé Moq, mais la même idée s’applique à n’importe quel framework fictif. 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 prérequis pour l’exécution du test.
  • Acte : effectuez le test.
  • Assert : vérifiez que le test a réussi.

À l’étape d’organisation, vous utiliserez souvent des objets fictifs ou stub. Cela réduit le nombre de dépendances, de sorte que le test se concentre 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.

Il s’agit de certains é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 d’action dans l’API web 2.

Test d’actions 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 référentiel 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 exemplaire factice 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 demande 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 d’itinéraire. Par conséquent, le test doit définir des valeurs pour celles-ci. Une autre option est de créer un mock ou un stub de UrlHelper. Avec cette approche, vous remplacez la valeur par défaut d’ApiController.Url par une version fictif 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 beaucoup de l’installation 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 quelques modèles courants utilisant IHttpActionResult. Voyons comment les tester unitairement.

L’action renvoie 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 le bon ID.

[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 correctement la réponse HTTP. (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 le statut 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, dans ce cas 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 retourner 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 retourner 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 vérifier le code d’état.

[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 additionnelles