Controladores de pruebas unitarias en ASP.NET Web API 2

En este tema se describen algunas técnicas específicas para controladores de pruebas unitarias en Web API 2. Antes de leer este tema, es posible que quiera leer el tutorial Unit Testing ASP.NET Web API 2, que muestra cómo agregar un proyecto de prueba unitaria a la solución.

Versiones de software usadas en el tutorial

Nota:

He usado Moq, pero la misma idea se aplica a cualquier framework de simulación. Moq 4.5.30 (y versiones posteriores) admite Visual Studio 2017, Roslyn y .NET 4.5 y versiones posteriores.

Un patrón común en las pruebas unitarias es "preparar-ejecutar-confirmar":

  • Organizar: configure los requisitos previos para que se ejecute la prueba.
  • Actúe: realice la prueba.
  • Aserción: compruebe que la prueba se realizó correctamente.

En el paso de organización, a menudo usará objetos ficticios o auxiliares. Esto minimiza el número de dependencias, por lo que la prueba se centra en probar una cosa.

Estas son algunas cosas que debes probar con pruebas unitarias en los controladores de la API web.

  • La acción devuelve el tipo correcto de respuesta.
  • Los parámetros no válidos devuelven la respuesta de error correcta.
  • La acción llama al método correcto en el repositorio o el nivel de servicio.
  • Si la respuesta incluye un modelo de dominio, compruebe el tipo de modelo.

Estos son algunos de los aspectos generales que se deben probar, pero los detalles dependen de la implementación del controlador. En concreto, hace una gran diferencia si las acciones del controlador devuelven HttpResponseMessage o IHttpActionResult. Para obtener más información sobre estos tipos de resultados, vea Resultados de la acción en Web Api 2.

Probar acciones que devuelven HttpResponseMessage

Este es un ejemplo de un controlador cuyas acciones devuelven 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;
    }
}

Observe que el controlador usa la inyección de dependencias para insertar un IProductRepository. Esto hace que el controlador sea más fácil de probar, ya que se puede inyectar un repositorio ficticio. La siguiente prueba unitaria verifica que el método Get escribe un Product en el cuerpo de la respuesta. Supongamos que repository es un simulacro 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);
}

Es importante establecer Solicitud y configuración en el controlador. De lo contrario, se producirá un error en la prueba con argumentNullException o InvalidOperationException.

El Post método llama a UrlHelper.Link para crear vínculos en la respuesta. Esto requiere un poco más de configuración en la prueba unitaria:

[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 clase UrlHelper necesita la dirección URL de solicitud y los datos de ruta, por lo que la prueba tiene que establecer valores para estos. Otra opción es mock o stub UrlHelper. Con este enfoque, se reemplaza el valor predeterminado de ApiController.Url por una versión ficticia o de código auxiliar que devuelve un valor fijo.

Vamos a reescribir la prueba mediante el marco moq . Instale el Moq paquete NuGet en el proyecto de prueba.

[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);
}

En esta versión, no es necesario configurar ningún dato de ruta, ya que urlHelper ficticio devuelve una cadena constante.

Probar acciones que devuelven IHttpActionResult

En la API web 2, una acción de controlador puede devolver IHttpActionResult, que es análoga a ActionResult en ASP.NET MVC. La interfaz IHttpActionResult define un patrón de comandos para crear respuestas HTTP. En lugar de crear la respuesta directamente, el controlador devuelve un IHttpActionResult. Más adelante, la canalización invoca IHttpActionResult para crear la respuesta. Este enfoque facilita la escritura de pruebas unitarias, ya que puede omitir una gran cantidad de la configuración necesaria para HttpResponseMessage.

Este es un controlador de ejemplo cuyas acciones devuelven 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);
    }    
}

En este ejemplo se muestran algunos patrones comunes mediante IHttpActionResult. Veamos cómo hacer pruebas unitarias.

Action devuelve 200 (Ok) con un cuerpo de respuesta

El Get método llama a Ok(product) si se encuentra el producto. En la prueba unitaria, asegúrese de que el tipo de valor devuelto sea OkNegotiatedContentResult y el producto devuelto tenga el identificador correcto.

[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);
}

Observe que la prueba unitaria no ejecuta el resultado de la acción. Puede suponer que el resultado de la acción crea correctamente la respuesta HTTP. (Es por eso que el marco de API web tiene sus propias pruebas unitarias).

Action devuelve 404 (no encontrado)

El Get método llama a NotFound() si no se encuentra el producto. En este caso, la prueba unitaria solo comprueba si el tipo de valor devuelto es 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));
}

Action devuelve 200 (OK) sin cuerpo de respuesta

El Delete método llama Ok() a para devolver una respuesta HTTP 200 vacía. Al igual que en el ejemplo anterior, la prueba unitaria comprueba el tipo de valor devuelto, en este caso 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));
}

Action devuelve 201 (Creado) con un encabezado Location

El método Post llama a CreatedAtRoute para devolver una respuesta HTTP 201 con un URI en el encabezado de Location. En la prueba unitaria, compruebe que la acción establece los valores de enrutamiento correctos.

[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"]);
}

Action devuelve otro 2xx con un cuerpo de respuesta

El Put método llama a Content para devolver una respuesta HTTP 202 (Aceptada) con un cuerpo de respuesta. Este caso es similar a devolver 200 (CORRECTO), pero la prueba unitaria también debe comprobar el código de estado.

[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);
}

Recursos adicionales