11

I'd like to "unit" test a method on my WebAPI contoller.

This method relies on a header being sent with it.

So

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

needs to have a value in the method body.

What is the best way of doing this? I thought I'd be able to set the ControllerContext which would populate HttpContext, but can't get it working.

I'd prefer not to use a mocking framework or any other third party tools, as my understanding is that WebAPI2 plays nicely with this use case.

I'm happy to set HttpContext.Current if that's the best way.

Ev.
  • 7,109
  • 14
  • 53
  • 87

5 Answers5

21

Hi I might be a little late to the party but I ran into the same problem and I here is what I ended up doing.

As others have noted, use Request.Headers instead of HttpCurrentContext in your controller actions e.g.

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

It is then really easy to create a HttpControllerContext and set the request property like this:

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

Hope this helps :)

P.s. Don't forget to add Web.Api.Core Reference to the test project :)

alliannas
  • 324
  • 3
  • 5
  • How do I do this for ASP.NET Core? My controller's ControllerContext is of type ControllerContext and not HttpControllerContext. – Josh Monreal Nov 21 '19 at 18:34
10

Sometimes, you have little/no control of the code you are writing tests for. If it's already been designed to use HttpContext.Current, and you keep getting "Operation is not supported on this platform." errors like i struggled with, this will help.

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

With that class in the same namespace, you can add the headers like:

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

Of course, if you don't like extension methods, you could always use a wrapper method instead.

I hope this helps someone.

mykeels
  • 590
  • 5
  • 9
6

Note: This answer works for the generic title of the question, however in this particular case the user has external code that relies on HttpContext.Current that is outside his control. If this is your case as well this is not the way to go. For most other users this is still recommended

Don't rely on HttpContext.Current in WebAPI. It's recommended in general to avoid using it in WebAPI, one of the main reasons is unit testability.

Also note I'm returning an IHttpActionResult that will make testing even easier.

Instead just use the controller member Request.Headers and then you can set it through the context object in your test

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
     }
 }

Here is an example for a full end-end in memory integration test (again note that you need to use the Request property that is available throughout the pipeline rather than HttpContext.Current. This code was taken from: WebAPI tests there a few more styles of integration tests in the code.

// 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);
Yishai Galatzer
  • 8,791
  • 2
  • 32
  • 41
  • 1
    Thanks! I've tried to set this, but Request is null. I tried: controller.Request.Headers.Add("value", "12345"); – Ev. May 07 '14 at 03:46
  • I added a sample. The request is only set at execution time, so for testing purposes you need to set it up yourself. – Yishai Galatzer May 07 '14 at 03:47
  • Thank you sir. That is exactly how simple I expected it to be. Unfortunately I also expected this to populate HttpContext.Current.Request.Headers, but it does not. Is there away to get this to propagate into the Context so that my "unit" test works downstream. (yes, it's more of an integration test). – Ev. May 07 '14 at 03:58
  • 1
    My point is, don't use HttpContext.Current in your code... The request flow down stream as well. Now if you are doing an integration test, you need to give some more info (like where do you start flowing your request in from?, and what is the end point) – Yishai Galatzer May 07 '14 at 04:01
  • I start my integration test at the controller, with new MyController().Get() then downstream I want to pull data out of the header. The code that uses the header info won't necessarily be called by a controller, but will definitely be some kind of web request. I was thinking that setting the header in the controller would populate the same space as a HttpContext.Current. – Ev. May 07 '14 at 05:36
  • You said that the "Request property that is available throughout the pipeline". How do I access the Request object from a class library? Is there a static version of it? At this point I'd no longer have access to the controller (as this downstream code will not necessarily be called from a controller). – Ev. May 08 '14 at 01:25
  • Where does it live in your app? It should have an interface that takes a request, if your code relies on HTTPcontext.current I'd recommend changing it if possible. – Yishai Galatzer May 08 '14 at 03:29
  • The HttpContext.Current is being called from an Aspect introduced via compile time weaving with PostSharp, which are basically attributes. So we're kind of limited to what we can pass to that aspect because of the language spec. – Ev. May 08 '14 at 04:54
  • yeah that's a problem. I'm not super familiar with PostSharp, but this doesn't seem to play nicely with Web API. – Yishai Galatzer May 08 '14 at 05:05
  • Yeah - basically I can't DI a HttpContextBase into the aspect, so I need to have an instance of it - which is why I'm using the HttpContext.Current. Is there a static HttpContextBase I can use instead so it plays nicely with the WebAPI Request stuff? – Ev. May 09 '14 at 00:59
  • Also, thanks again for the help/time/effort/input and all that. :) – Ev. May 09 '14 at 00:59
  • That's the problem with "nice" abstractions, sometimes they leak :) Sorry but I don't know how to help from here, you might want to open a bug/question on PostSharp itself – Yishai Galatzer May 09 '14 at 01:33
  • That's cool. Thanks again. And just to be clear - I don't think this is a shortcoming of PostSharp (which is a great tool) it's the implementation of this particular aspect - it needs to provide a way to DI the HttpContext. Cheers. – Ev. May 09 '14 at 02:17
3

I would suggest it.
While you create your private controller objects set these settings at that time.

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

You can mock the HTTP request context. Are you using a mocking framework like Moq? That makes it easy to mock the request headers collection. If not using Moq, see this SO question.

Community
  • 1
  • 1
abjbhat
  • 999
  • 3
  • 13
  • 24
  • Thanks for the answer! I'm trying not to use a mocking framework, but it looks like I might have to. Thanks for the resource. – Ev. May 08 '14 at 01:11
  • Huh? Oh man, no idea. I didn't down vote you. Must have been some jerk. People seem to love dishing out downvotes - I asked a question a while back that go like 5 downvotes but no one bothered to tell me why. Here - have an upvote from me. – Ev. May 09 '14 at 00:54
  • Oh, ok. I hope I could help. – abjbhat May 09 '14 at 12:47