Setting HttpContext.Current.Session in a unit test

2019-01-01 08:16发布

I have a web service I am trying to unit test. In the service it pulls several values from the HttpContext like so:

 m_password = (string)HttpContext.Current.Session["CustomerId"];
 m_userID = (string)HttpContext.Current.Session["CustomerUrl"];

in the unit test I am creating the context using a simple worker request, like so:

SimpleWorkerRequest request = new SimpleWorkerRequest("", "", "", null, new StringWriter());
HttpContext context = new HttpContext(request);
HttpContext.Current = context;

However, whenever I try to set the values of HttpContext.Current.Session

HttpContext.Current.Session["CustomerId"] = "customer1";
HttpContext.Current.Session["CustomerUrl"] = "customer1Url";

I get null reference exception that says HttpContext.Current.Session is null.

Is there any way to initialize the current session within the unit test?

13条回答
梦寄多情
2楼-- · 2019-01-01 08:23

We had to mock HttpContext by using a HttpContextManager and calling the factory from within our application as well as the Unit Tests

public class HttpContextManager 
{
    private static HttpContextBase m_context;
    public static HttpContextBase Current
    {
        get
        {
            if (m_context != null)
                return m_context;

            if (HttpContext.Current == null)
                throw new InvalidOperationException("HttpContext not available");

            return new HttpContextWrapper(HttpContext.Current);
        }
    }

    public static void SetCurrentContext(HttpContextBase context)
    {
        m_context = context;
    }
}

You would then replace any calls to HttpContext.Current with HttpContextManager.Current and have access to the same methods. Then when you're testing, you can also access the HttpContextManager and mock your expectations

This is an example using Moq:

private HttpContextBase GetMockedHttpContext()
{
    var context = new Mock<HttpContextBase>();
    var request = new Mock<HttpRequestBase>();
    var response = new Mock<HttpResponseBase>();
    var session = new Mock<HttpSessionStateBase>();
    var server = new Mock<HttpServerUtilityBase>();
    var user = new Mock<IPrincipal>();
    var identity = new Mock<IIdentity>();
    var urlHelper = new Mock<UrlHelper>();

    var routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    var requestContext = new Mock<RequestContext>();
    requestContext.Setup(x => x.HttpContext).Returns(context.Object);
    context.Setup(ctx => ctx.Request).Returns(request.Object);
    context.Setup(ctx => ctx.Response).Returns(response.Object);
    context.Setup(ctx => ctx.Session).Returns(session.Object);
    context.Setup(ctx => ctx.Server).Returns(server.Object);
    context.Setup(ctx => ctx.User).Returns(user.Object);
    user.Setup(ctx => ctx.Identity).Returns(identity.Object);
    identity.Setup(id => id.IsAuthenticated).Returns(true);
    identity.Setup(id => id.Name).Returns("test");
    request.Setup(req => req.Url).Returns(new Uri("http://www.google.com"));
    request.Setup(req => req.RequestContext).Returns(requestContext.Object);
    requestContext.Setup(x => x.RouteData).Returns(new RouteData());
    request.SetupGet(req => req.Headers).Returns(new NameValueCollection());

    return context.Object;
}

and then to use it within your unit tests, I call this within my Test Init method

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

you can then, in the above method add the expected results from Session that you're expecting to be available to your web service.

查看更多
浅入江南
3楼-- · 2019-01-01 08:27

You can "fake it" by creating a new HttpContext like this:

http://www.necronet.org/archive/2010/07/28/unit-testing-code-that-uses-httpcontext-current-session.aspx

I've taken that code and put it on an static helper class like so:

public static HttpContext FakeHttpContext()
{
    var httpRequest = new HttpRequest("", "http://stackoverflow/", "");
    var stringWriter = new StringWriter();
    var httpResponse = new HttpResponse(stringWriter);
    var httpContext = new HttpContext(httpRequest, httpResponse);

    var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
                                            new HttpStaticObjectsCollection(), 10, true,
                                            HttpCookieMode.AutoDetect,
                                            SessionStateMode.InProc, false);

    httpContext.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
                                BindingFlags.NonPublic | BindingFlags.Instance,
                                null, CallingConventions.Standard,
                                new[] { typeof(HttpSessionStateContainer) },
                                null)
                        .Invoke(new object[] { sessionContainer });

    return httpContext;
}

Or instead of using reflection to construct the new HttpSessionState instance, you can just attach your HttpSessionStateContainer to the HttpContext (as per Brent M. Spell's comment):

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

and then you can call it in your unit tests like:

HttpContext.Current = MockHelper.FakeHttpContext();
查看更多
无与为乐者.
4楼-- · 2019-01-01 08:28

The answer @Ro Hit gave helped me a lot, but I was missing the user credentials because I had to fake a user for authentication unit testing. Hence, let me describe how I solved it.

According to this, if you add the method

    // using System.Security.Principal;
    GenericPrincipal FakeUser(string userName)
    {
        var fakeIdentity = new GenericIdentity(userName);
        var principal = new GenericPrincipal(fakeIdentity, null);
        return principal;
    }

and then append

    HttpContext.Current.User = FakeUser("myDomain\\myUser");

to the last line of the TestSetup method you're done, the user credentials are added and ready to be used for authentication testing.

I also noticed that there are other parts in HttpContext you might require, such as the .MapPath() method. There is a FakeHttpContext available, which is described here and can be installed via NuGet.

查看更多
查无此人
5楼-- · 2019-01-01 08:33

Milox solution is better than the accepted one IMHO but I had some problems with this implementation when handling urls with querystring.

I made some changes to make it work properly with any urls and to avoid Reflection.

public static HttpContext FakeHttpContext(string url)
{
    var uri = new Uri(url);
    var httpRequest = new HttpRequest(string.Empty, uri.ToString(),
                                        uri.Query.TrimStart('?'));
    var stringWriter = new StringWriter();
    var httpResponse = new HttpResponse(stringWriter);
    var httpContext = new HttpContext(httpRequest, httpResponse);

    var sessionContainer = new HttpSessionStateContainer("id",
                                    new SessionStateItemCollection(),
                                    new HttpStaticObjectsCollection(),
                                    10, true, HttpCookieMode.AutoDetect,
                                    SessionStateMode.InProc, false);

    SessionStateUtility.AddHttpSessionStateToContext(
                                         httpContext, sessionContainer);

    return httpContext;
}
查看更多
泪湿衣
6楼-- · 2019-01-01 08:33

In asp.net Core / MVC 6 rc2 you can set the HttpContext

var SomeController controller = new SomeController();

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext();
controller.HttpContext.Session = new DummySession();

rc 1 was

var SomeController controller = new SomeController();

controller.ActionContext = new ActionContext();
controller.ActionContext.HttpContext = new DefaultHttpContext();
controller.HttpContext.Session = new DummySession();

https://stackoverflow.com/a/34022964/516748

Consider using Moq

new Mock<ISession>();
查看更多
妖精总统
7楼-- · 2019-01-01 08:35

Never mock.. never! The solution is pretty simple. Why fake such a beautiful creation like HttpContext?

Push the session down! (Just this line is enough for most of us to understand but explained in detail below)

(string)HttpContext.Current.Session["CustomerId"]; is how we access it now. Change this to

_customObject.SessionProperty("CustomerId")

When called from test, _customObject uses alternative store (DB or cloud key value[ http://www.kvstore.io/] )

But when called from the real application, _customObject uses Session.

how is this done? well... Dependency Injection!

So test can set the session(underground) and then call the application method as if it knows nothing about the session. Then test secretly checks if the application code correctly updated the session. Or if the application behaves based on the session value set by the test.

Actually, we did end up mocking even though I said: "never mock". Becuase we couldn't help but slip to the next rule, "mock where it hurts the least!". Mocking huge HttpContext or mocking a tiny session, which hurts the least? don't ask me where these rules came from. Let us just say common sense. Here is an interesting read on not mocking as unit test can kills us

查看更多
登录 后发表回答