【问题标题】:Unit testing a WebAPI2 controller method with a header value使用标头值对 WebAPI2 控制器方法进行单元测试
【发布时间】:2014-06-23 20:48:54
【问题描述】:

我想在我的 WebAPI 控制器上“单元”测试一个方法。

此方法依赖于与它一起发送的标头。

所以

HttpContext.Current.Request.Headers["name"]

需要在方法体中有一个值。

这样做的最佳方法是什么?我以为我可以设置将填充 HttpContext 的 ControllerContext,但无法使其正常工作。

我不希望使用模拟框架或任何其他第三方工具,因为我的理解是 WebAPI2 可以很好地处理这个用例。

如果这是最好的方法,我很乐意设置 HttpContext.Current。

【问题讨论】:

    标签: c# asp.net unit-testing asp.net-web-api asp.net-web-api2


    【解决方案1】:

    嗨,我参加聚会可能有点晚了,但我遇到了同样的问题,这就是我最终要做的。

    正如其他人所指出的,在控制器操作中使用 Request.Headers 而不是 HttpCurrentContext,例如

        [Route("")]
        [HttpGet]
        public IHttpActionResult Get()
        {
            // The header can have multiple values, I call SingleOrDefault as I only expect 1 value.
            var myHeader = Request.Headers.GetValues("X-My-Header").SingleOrDefault();
            if (myHeader == "success")
            {
                 return Ok<string>("Success!");
            }
    
             return BadRequest();
        }
    

    这样就很容易创建一个 HttpControllerContext 并像这样设置请求属性:

    [TestMethod]
    public void Get_HeaderIsValid()
    {
        // Arrange
        var controller = new ConfigurationsController(null);
        var controllerContext = new HttpControllerContext();
        var request = new HttpRequestMessage();
        request.Headers.Add("X-My-Header", "success");
    
        // Don't forget these lines, if you do then the request will be null.
        controllerContext.Request = request;
        controller.ControllerContext = controllerContext;
    
        // Act
        var result = controller.Get() as OkNegotiatedContentResult<string>;
    
        // Assert
        Assert.IsNotNull(result);
        Assert.AreEqual("Success!", result.Content);
    }
    

    希望这会有所帮助:)

    附:不要忘记在测试项目中添加 Web.Api.Core Reference :)

    【讨论】:

    • 如何为 ASP.NET Core 执行此操作?我的控制器的 ControllerContext 是 ControllerContext 类型,而不是 HttpControllerContext。
    【解决方案2】:

    有时,您几乎/无法控制正在为其编写测试的代码。如果它已经被设计为使用HttpContext.Current,并且您不断收到"Operation is not supported on this platform." 错误,就像我遇到的那样,这将有所帮助。

    public static class NameValueCollectionExtensions
    {
        public static NameValueCollection AddValue(this NameValueCollection headers, string key, string value)
        {
            Type t = headers.GetType();
            t.InvokeMember("MakeReadWrite", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, headers, null);
            t.InvokeMember("InvalidateCachedArrays", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, headers, null);
            t.InvokeMember("BaseAdd", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, headers, new object[] { key, new System.Collections.ArrayList() { value } });
            t.InvokeMember("MakeReadOnly", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, headers, null);
            return headers;
        }
    }
    

    在同一个命名空间中使用该类,您可以添加如下标题:

    HttpContext.Current.Request.Headers.AddValue("header_key", "header_value");
    

    当然,如果您不喜欢扩展方法,您可以随时使用包装方法。

    我希望这对某人有所帮助。

    【讨论】:

    • 这里的最佳答案,因为所有其他答案都假设您使用的是控制器上下文,但情况并非总是如此。因此,来自目前未使用 MVC 的人 +1。
    • +1 提供迄今为止最简单的解决方案,不依赖任何外部库并且始终有效
    • 出色的破解。对于 ServerVariables,我必须使用 t.InvokeMember("AddStatic", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, binder: null, target: headers, args: new object[] {key, value });而不是 BaseAdd
    • HttpContext.Current.Request.Headers.AddValue("header_key", "header_value"); 如何/在哪里定义/创建?
    • 拯救了我们的今天。谢谢
    【解决方案3】:

    注意:此答案适用于问题的通用标题,但在这种特殊情况下,用户有外部代码依赖于他无法控制的 HttpContext.Current。如果这也是你的情况,这不是要走的路。对于大多数其他用户,仍然建议这样做

    不要依赖 WebAPI 中的 HttpContext.Current。一般建议避免在 WebAPI 中使用它,主要原因之一是单元可测试性。

    另请注意,我将返回一个 IHttpActionResult,这将使测试更加容易。

    只需使用控制器成员Request.Headers,然后您可以通过测试中的上下文对象进行设置

    public class MyController : ApiController
    {
        public IHttpActionResult Get()
        {
             if (Request.Headers. /* insert your code here */)
             {
                 // Do Something
             }
        }
     }
    
     public class TestClass
     {
         public void Test()
         {
             // Arrange
             var controller = new MyController();
             var request = new HttpRequestMessage();
             request.Headers... // setup your test here
    
             // Act
             var result = controller.Get();
    
             // Assert
             // Verify here
         }
     }
    

    这是一个完整的内存集成测试的端到端示例(再次注意,您需要使用整个管道中可用的 Request 属性而不是 HttpContext.Current。此代码取自:WebAPI tests there代码中还有更多样式的集成测试。

    // Do any setup work
    HttpConfiguration config = new HttpConfiguration();
    config.Routes.MapHttpRoute("Default", "{controller}/{action}");
    
    // Setup in memory server and client
    HttpServer server = new HttpServer(config);
    HttpClient client = new HttpClient(server);
    
    // Act
    HttpResponseMessage response = client.GetAsync("http://localhost/" + requestUrl).Result;
    
    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    Assert.Equal(count, response.Content.ReadAsAsync<int>().Result);
    

    【讨论】:

    • 谢谢!我试图设置它,但请求为空。我试过了:controller.Request.Headers.Add("value", "12345");
    • 我添加了一个示例。该请求仅在执行时设置,因此出于测试目的,您需要自行设置。
    • 谢谢先生。这正是我所期望的简单。不幸的是,我还希望这会填充 HttpContext.Current.Request.Headers,但事实并非如此。是否可以将其传播到上下文中,以便我的“单元”测试在下游工作。 (是的,它更像是一个集成测试)。
    • 我的意思是,不要在你的代码中使用 HttpContext.Current ......请求也顺流而下。现在,如果您正在进行集成测试,则需要提供更多信息(例如,您从哪里开始输入请求?以及终点是什么)
    • 我在控制器上开始我的集成测试,使用 new MyController().Get() 然后下游我想从标题中提取数据。使用头信息的代码不一定会被控制器调用,但肯定是某种网络请求。我在想在控制器中设置标题会填充与 HttpContext.Current 相同的空间。
    【解决方案4】:

    我会推荐它。
    当您创建私有控制器对象时,请同时设置这些设置。

        private HomeController createHomeController()
        {
            var httpContext = new DefaultHttpContext();
            httpContext.Request.Headers["Key"] = "value123";
            var controllerContext = new ControllerContext
            {
                HttpContext = httpContext
            };
              
            return new HomeController()
            {
                ControllerContext = controllerContext
            };
        }
    

    【讨论】:

      【解决方案5】:

      您可以模拟 HTTP 请求上下文。你在使用像 Moq 这样的模拟框架吗?这使得模拟请求标头集合变得容易。如果不使用起订量,请参阅此 SO question

      【讨论】:

      • 感谢您的回答!我试图不使用模拟框架,但看起来我可能不得不这样做。感谢您的资源。
      • 嗯?哦,伙计,不知道。我没有对你投反对票。一定是个混蛋。人们似乎很喜欢投反对票——我不久前问了一个问题,好像投了 5 票,但没人愿意告诉我为什么。在这里 - 让我投赞成票。
      • 哦,好的。我希望我能帮上忙。
      猜你喜欢
      • 2014-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多