通过


ASP.NET Web API 2 中的单元测试控制器

本主题介绍 Web API 2 中单元测试控制器的一些特定技术。 阅读本主题之前,可能需要阅读教程 单元测试 ASP.NET Web API 2,其中显示了如何将单元测试项目添加到解决方案。

本教程中使用的软件版本

注释

我使用了 Moq,但相同的想法适用于任何模拟框架。 Moq 4.5.30(及更高版本)支持 Visual Studio 2017、Roslyn 和 .NET 4.5 及更高版本。

单元测试中的常见模式是“安排-执行-断言(arrange-act-assert)”:

  • 安排:设置测试运行的任何先决条件。
  • 操作:执行测试。
  • 断言:验证测试是否成功。

在排列步骤中,通常使用模拟或存根对象。 这可最大程度地减少依赖项数量,因此测试侧重于测试一件事。

下面是您在 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);
}

必须在控制器上设置 请求配置 。 否则,测试将失败并显示 ArgumentNullExceptionInvalidOperationException

该方法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 的默认值替换为返回固定值的模拟或存根版本。

让我们使用 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 中的 ActionResultIHttpActionResult 接口定义用于创建 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(已创建),并附有 Location 标头

方法 Post 调用 CreatedAtRoute,返回一个带有 URI 的 HTTP 201 响应,并在 Location 标头中指定。 在单元测试中,验证操作是否设置正确的路由值。

[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(已接受)响应。 这种情况类似于返回 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);
}

其他资源