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
- Visual Studio 2017
- API web 2
- Moq 4.5.30
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.
Test de la génération de liens
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);
}