ASP.NET Web API 2 中的单元测试控制器
本主题介绍 Web API 2 中单元测试控制器的一些特定技术。 在阅读本主题之前,可能需要阅读单元测试 ASP.NET Web API 2 教程,其中演示如何将单元测试项目添加到解决方案。
本教程中使用的软件版本
- Visual Studio 2017
- Web API 2
- Moq 4.5.30
注意
我使用了 Moq,但同样的想法也适用于任何模拟框架。 Moq 4.5.30 (及更高版本) 支持 Visual Studio 2017、Roslyn 和 .NET 4.5 及更高版本。
单元测试中的常见模式是“arrange-act-assert”:
- 排列:设置测试运行的任何先决条件。
- 操作:执行测试。
- 断言:验证测试是否成功。
在排列步骤中,通常会使用 mock 或存根对象。 这最大限度地减少了依赖项的数量,因此测试侧重于测试一件事。
下面是应在 Web API 控制器中进行单元测试的一些内容:
- 操作返回正确类型的响应。
- 无效参数返回正确的错误响应。
- 操作在存储库或服务层上调用正确的方法。
- 如果响应包含域模型,请验证模型类型。
以下是要测试的一些常规内容,但具体内容取决于控制器实现。 具体而言,控制器操作返回 HttpResponseMessage 还是 IHttpActionResult 都有很大的不同。 有关这些结果类型的详细信息,请参阅 Web Api 2 中的操作结果。
测试返回 HttpResponseMessage 的操作
下面是其操作返回 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;
}
}
请注意,控制器使用依赖关系注入来注入 IProductRepository
。 这使得控制器更易于测试,因为可以注入模拟存储库。 以下单元测试验证方法是否 Get
将 写入 Product
响应正文。 repository
假设 是模拟 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);
}
请务必在控制器上设置“请求”和“配置”。 否则,测试将失败并显示 ArgumentNullException 或 InvalidOperationException。
测试链接生成
方法 Post
调用 UrlHelper.Link 在响应中创建链接。 这需要在单元测试中进行更多的设置:
[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);
}
UrlHelper 类需要请求 URL 和路由数据,因此测试必须设置这些值。 另一个选项是模拟或存根 UrlHelper。 使用此方法,可将 默认值 ApiController.Url 替换为返回固定值的 mock 或存根版本。
让我们使用 Moq 框架重写测试。 在 Moq
测试项目中安装 NuGet 包。
[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);
}
在此版本中,无需设置任何路由数据,因为模拟 UrlHelper 返回常量字符串。
测试返回 IHttpActionResult 的操作
在 Web API 2 中,控制器操作可以返回 IHttpActionResult,这类似于 ASP.NET MVC 中的 ActionResult 。 IHttpActionResult 接口定义用于创建 HTTP 响应的命令模式。 控制器不直接创建响应,而是返回 IHttpActionResult。 稍后,管道调用 IHttpActionResult 来创建响应。 此方法使编写单元测试更加容易,因为可以跳过 HttpResponseMessage 所需的大量设置。
下面是一个示例控制器,其操作返回 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);
}
}
此示例演示使用 IHttpActionResult 的一些常见模式。 让我们看看如何对它们进行单元测试。
操作使用响应正文返回 200 (正常)
如果找到产品, 方法 Get
将调用 Ok(product)
。 在单元测试中,确保返回类型为 OkNegotiatedContentResult ,并且返回的产品具有正确的 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);
}
请注意,单元测试不会执行操作结果。 可以假设操作结果正确创建了 HTTP 响应。 (这就是 Web API 框架有自己的单元测试的原因!)
操作返回 404 (找不到)
如果未找到产品,则 Get
方法将调用 NotFound()
。 对于这种情况,单元测试只检查返回类型是否为 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));
}
操作返回 200 (正常) ,没有响应正文
方法 Delete
调用 Ok()
以返回空的 HTTP 200 响应。 与前面的示例一样,单元测试检查返回类型,在本例中为 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));
}
操作返回 201 (Created) 带有 Location 标头
方法 Post
调用 CreatedAtRoute
以返回 HTTP 201 响应,并在 Location 标头中包含 URI。 在单元测试中,验证操作是否设置了正确的路由值。
[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"]);
}
操作返回另一个包含响应正文的 2xx
方法 Put
调用 Content
以使用响应正文返回 HTTP 202 (Accepted) 响应。 这种情况类似于返回 200 (正常) ,但单元测试还应检查状态代码。
[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);
}
其他资源
- 单元测试 ASP.NET Web API 2 时模拟实体框架
- Youssef Moussaoui) 的博客文章 (编写 ASP.NET Web API服务测试。
- Debugging ASP.NET Web API with Route Debugger(使用路由调试器调试 ASP.NET Web API)