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 Pruebas unitarias de ASP.NET Web API 2, que muestra cómo agregar un proyecto de pruebas unitarias a la solución.
Versiones de software usadas en el tutorial
- Visual Studio 2017
- Web API 2
- Moq 4.5.30
Nota:
He usado Moq, pero la misma idea se aplica a cualquier marco 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 "organizar-actuar-afirmar":
- Organizar: configurar los requisitos previos para que se ejecute la prueba.
- Actuar: realizar la prueba.
- Afirmar: comprobar que la prueba se ha realizado 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 para las que debe realizar pruebas unitarias en los controladores de Web API:
- 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 del repositorio o la capa de servicio.
- Si la respuesta incluye un modelo de dominio, compruebe el tipo de modelo.
Estos son algunos de las cuestiones generales que se deben probar, pero los detalles dependen de la implementación del controlador. Específicamente, hay una gran diferencia si las acciones del controlador devuelven HttpResponseMessage o IHttpActionResult. Para obtener más información sobre estos tipos de resultados, consulte Resultados de acciones en Web Api 2.
Prueba de 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 inserción de dependencias para insertar un elemento IProductRepository
. Esto hace que controlador tenga más capacidad de prueba, ya que puede insertar un repositorio ficticio. La siguiente prueba unitaria comprueba que el método Get
escriba un elemento Product
en el cuerpo de la respuesta. Supongamos que repository
es un elemento IProductRepository
ficticio.
[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 los elementos Request y Configuration en el controlador. De lo contrario, se producirá en la prueba un error ArgumentNullException o InvalidOperationException.
Prueba de la generación de vínculos
El método Post
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 la solicitud y los datos de la ruta, por lo que la prueba tiene que establecer valores para ellos. Otra opción es un elemento UrlHelper ficticio o auxiliar. 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 paquete NuGet Moq
en el proyecto de pruebas.
[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 el elementoUrlHelper ficticio devuelve una cadena constante.
Prueba de acciones que devuelven IHttpActionResult
En Web API 2, una acción del controlador puede devolver IHttpActionResult, que es análogo a ActionResult en MVC de ASP.NET. La interfaz IHttpActionResult define un patrón de comandos para crear respuestas HTTP. En lugar de crear la respuesta directamente, el controlador devuelve un elemento IHttpActionResult. Más adelante, la canalización invoca a 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 con IHttpActionResult. Veamos cómo realizar pruebas unitarias en ellos.
La acción devuelve 200 (OK) con un cuerpo de respuesta
El método Get
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 de que 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. Se puede dar por supuesto que el resultado de la acción crea correctamente la respuesta HTTP. (Es por eso que el marco Web API tiene sus propias pruebas unitarias).
La acción devuelve 404 (No encontrado)
El método Get
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));
}
La acción devuelve 200 (OK) sin cuerpo de respuesta
El método Delete
llama a Ok()
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));
}
La acción devuelve 201 (Creado) con un encabezado de ubicación
El método Post
llama a CreatedAtRoute
para devolver una respuesta HTTP 201 con un identificador URI en el encabezado de ubicación. En la prueba unitaria, compruebe que la acción establezca 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"]);
}
La acción devuelve otra respuesta 2xx con un cuerpo de respuesta
El método Put
llama a Content
para devolver una respuesta HTTP 202 (aceptada) con un cuerpo de respuesta. Este caso es similar a devolver 200 (OK), 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);
}