在单元测试中设置HttpContext.Current.Session

212

我有一个正在尝试进行单元测试的Web服务。在服务中,它从HttpContext中提取多个值,如下所示:

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

在我的单元测试中,我使用简单的工作者请求来创建上下文,就像这样:

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

然而,每当我尝试设置HttpContext.Current.Session的值时

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

我得到了一个空引用异常,指出HttpContext.Current.Session为空。

是否有任何方法在单元测试中初始化当前会话?


你尝试过这个方法了吗? - Raj Ranjhan
如果可以的话,请使用HttpContextBase - jrummell
14个回答

333
你可以通过创建一个新的 HttpContext 来“虚拟”它,具体方法如下:

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

我已经将这段代码放在一个静态帮助类中,代码如下:
public static HttpContext FakeHttpContext()
{
    var httpRequest = new HttpRequest("", "http://example.com/", "");
    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;
}

或者,您可以像Brent M. Spell的评论中所述,将HttpSessionStateContainer附加到HttpContext上,而不是使用反射来构造新的HttpSessionState实例:

SessionStateUtility.AddHttpSessionStateToContext(httpContext, sessionContainer);

然后你可以在单元测试中这样调用它:

HttpContext.Current = MockHelper.FakeHttpContext();

31
我喜欢这个回答胜过被采纳的那个,因为改变你的生产代码以支持测试活动是不好的做法。诚然,你的生产代码应该像这样抽象出第三方命名空间,但当你在处理遗留代码时,你并不总是拥有这种控制或者重构的奢侈条件。 - Sean Glover
31
你不需要使用反射来构造新的HttpSessionState实例。你可以使用SessionStateUtility.AddHttpSessionStateToContext将你的HttpSessionStateContainer附加到HttpContext上。 - Brent M. Spell
MockHelper只是包含静态方法的类的名称,你可以使用任何你喜欢的名称。 - Juan Jimenez
有什么想法可以使用这种方法写入到 HttpRequest.InputStream 吗? - Steven de Salas
这个答案在被采纳的答案之后起作用了。谢谢 :) - Omar.Ebrahim
显示剩余5条评论

113

我们不得不使用一个 HttpContextManager 并从应用程序以及单元测试中调用工厂来模拟 HttpContext

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;
    }
}
你可以将任何对 HttpContext.Current 的调用替换为 HttpContextManager.Current,然后就可以访问相同的方法。在测试时,你也可以访问 HttpContextManager 并模拟你的预期。
这是一个使用 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;
}

然后在你的单元测试中使用它,我在我的测试初始化方法中调用它

HttpContextManager.SetCurrentContext(GetMockedHttpContext());

你可以在上述方法中添加你希望在Web服务中可用的来自 Session 的预期结果。


1
但是这不使用SimpleWorkerRequest。 - knocte
m_context 后备字段仅在模拟上下文(通过 SetCurrentContext 设置)返回,对于真正的 HttpContext,每次调用 Current 都会创建一个包装器,这是有意为之吗? - Stephen Price
我认为这个类不应该被称为工厂,而应该取一个别的名字,比如HttpContextSource,因为它并不创建新的对象。 - user1713059
1
“HttpContextManager” 这个名称比 “HttpContextSource” 更好,但我同意 “HttpContextFactory” 是具有误导性的。 - Professor of programming
@AnthonyShaw:我已经实现了你的代码,但在Mvc Action中仍然出现“HttpContext.current”为空的情况。 - Amit Kumar
显示剩余2条评论

50

Milox解决方案在我看来比被接受的解决方案更好,但是当处理带有查询字符串的url时,我使用此实现遇到了一些问题。

我进行了一些更改,使其能够正常处理任何url并避免使用反射。

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

1
这允许您伪造httpContext.Session,有没有什么方法可以做到同样的事情来处理httpContext.Application - KyleMit
无法创建抽象类型或接口“HttpContext”的实例。在我的单元测试中,我应该添加什么? - toha

44

我之前写过相关的内容。

在MVC3 .NET中单元测试HttpContext.Current.Session

希望对你有所帮助。

[TestInitialize]
public void TestSetup()
{
    // We need to setup the Current HTTP Context as follows:            
 
    // Step 1: Setup the HTTP Request
    var httpRequest = new HttpRequest("", "http://localhost/", "");
 
    // Step 2: Setup the HTTP Response
    var httpResponce = new HttpResponse(new StringWriter());
 
    // Step 3: Setup the Http Context
    var httpContext = new HttpContext(httpRequest, httpResponce);
    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 });
 
    // Step 4: Assign the Context
    HttpContext.Current = httpContext;
}

[TestMethod]
public void BasicTest_Push_Item_Into_Session()
{
    // Arrange
    var itemValue = "RandomItemValue";
    var itemKey = "RandomItemKey";
             
    // Act
    HttpContext.Current.Session.Add(itemKey, itemValue);
             
    // Assert
    Assert.AreEqual(HttpContext.Current.Session[itemKey], itemValue);
}

工作得非常好和简单...谢谢! - mggSoft

15

您可以尝试使用FakeHttpContext

using (new FakeHttpContext())
{
   HttpContext.Current.Session["CustomerId"] = "customer1";       
}

2
非常好用,而且非常简单易懂。 - Beanwah
2
不幸的是,与.NET Core不兼容。 - Luis Gouveia
@LuisGouveia,.NET Core是否有任何这样的问题? - vAD

15
在asp.net Core / MVC 6 rc2中,您可以设置HttpContext
var SomeController controller = new SomeController();

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

rc 1是

var SomeController controller = new SomeController();

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

https://dev59.com/610a5IYBdhLWcg3wPWhN#34022964

考虑使用 Moq
new Mock<ISession>();

13
如果您正在使用MVC框架,那么这应该可以运行。我使用了Milox的FakeHttpContext并添加了几行额外的代码。 这个想法来自于这篇帖子:

http://codepaste.net/p269t8

这似乎在MVC 5中有效。我没有在早期版本的MVC中尝试过。
HttpContext.Current = MockHttpContext.FakeHttpContext();

var wrapper = new HttpContextWrapper(HttpContext.Current);

MyController controller = new MyController();
controller.ControllerContext = new ControllerContext(wrapper, new RouteData(), controller);

string result = controller.MyMethod();

4
链接已损坏,下次可以将代码放在这里。 - Rhyous

7

The answer that worked with me is what @Anthony had written, but you have to add another line which is

    request.SetupGet(req => req.Headers).Returns(new NameValueCollection());

so you can use this:

HttpContextFactory.Current.Request.Headers.Add(key, value);


2
尝试这个:

        // MockHttpSession Setup
        var session = new MockHttpSession();

        // MockHttpRequest Setup - mock AJAX request
        var httpRequest = new Mock<HttpRequestBase>();

        // Setup this part of the HTTP request for AJAX calls
        httpRequest.Setup(req => req["X-Requested-With"]).Returns("XMLHttpRequest");

        // MockHttpContextBase Setup - mock request, cache, and session
        var httpContext = new Mock<HttpContextBase>();
        httpContext.Setup(ctx => ctx.Request).Returns(httpRequest.Object);
        httpContext.Setup(ctx => ctx.Cache).Returns(HttpRuntime.Cache);
        httpContext.Setup(ctx => ctx.Session).Returns(session);

        // MockHttpContext for cache
        var contextRequest = new HttpRequest("", "http://localhost/", "");
        var contextResponse = new HttpResponse(new StringWriter());
        HttpContext.Current = new HttpContext(contextRequest, contextResponse);

        // MockControllerContext Setup
        var context = new Mock<ControllerContext>();
        context.Setup(ctx => ctx.HttpContext).Returns(httpContext.Object);

        //TODO: Create new controller here
        //      Set controller's ControllerContext to context.Object

并添加类:

public class MockHttpSession : HttpSessionStateBase
{
    Dictionary<string, object> _sessionDictionary = new Dictionary<string, object>();
    public override object this[string name]
    {
        get
        {
            return _sessionDictionary.ContainsKey(name) ? _sessionDictionary[name] : null;
        }
        set
        {
            _sessionDictionary[name] = value;
        }
    }

    public override void Abandon()
    {
        var keys = new List<string>();

        foreach (var kvp in _sessionDictionary)
        {
            keys.Add(kvp.Key);
        }

        foreach (var key in keys)
        {
            _sessionDictionary.Remove(key);
        }
    }

    public override void Clear()
    {
        var keys = new List<string>();

        foreach (var kvp in _sessionDictionary)
        {
            keys.Add(kvp.Key);
        }

        foreach(var key in keys)
        {
            _sessionDictionary.Remove(key);
        }
    }
}

这将允许您同时测试会话和缓存。

2

我希望找到一种比上述选项更轻侵入的方法。最终,我想出了一个有点俗气但可能会让某些人加速的解决方案。

首先,我创建了一个TestSession类:

class TestSession : ISession
{

    public TestSession()
    {
        Values = new Dictionary<string, byte[]>();
    }

    public string Id
    {
        get
        {
            return "session_id";
        }
    }

    public bool IsAvailable
    {
        get
        {
            return true;
        }
    }

    public IEnumerable<string> Keys
    {
        get { return Values.Keys; }
    }

    public Dictionary<string, byte[]> Values { get; set; }

    public void Clear()
    {
        Values.Clear();
    }

    public Task CommitAsync()
    {
        throw new NotImplementedException();
    }

    public Task LoadAsync()
    {
        throw new NotImplementedException();
    }

    public void Remove(string key)
    {
        Values.Remove(key);
    }

    public void Set(string key, byte[] value)
    {
        if (Values.ContainsKey(key))
        {
            Remove(key);
        }
        Values.Add(key, value);
    }

    public bool TryGetValue(string key, out byte[] value)
    {
        if (Values.ContainsKey(key))
        {
            value = Values[key];
            return true;
        }
        value = new byte[0];
        return false;
    }
}

然后我在控制器的构造函数中添加了一个可选参数。如果该参数存在,则用它来操作会话。否则,使用HttpContext.Session:

class MyController
{

    private readonly ISession _session;

    public MyController(ISession session = null)
    {
        _session = session;
    }


    public IActionResult Action1()
    {
        Session().SetString("Key", "Value");
        View();
    }

    public IActionResult Action2()
    {
        ViewBag.Key = Session().GetString("Key");
        View();
    }

    private ISession Session()
    {
        return _session ?? HttpContext.Session;
    }
}

现在我可以将我的TestSession注入到控制器中:
class MyControllerTest
{

    private readonly MyController _controller;

    public MyControllerTest()
    {
        var testSession = new TestSession();
        var _controller = new MyController(testSession);
    }
}

1
我真的很喜欢你的解决方案。KISS => 保持简单和愚蠢;-) - CodeNotFound

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接