等待后HttpContext.Current为null(仅在单元测试中)

时间:2014-09-30 10:40:59

标签: c# asp.net-mvc unit-testing async-await

我正在为MVC 5 Web应用程序编写单元测试。我从我的测试中嘲笑了HttpContext.Current。 当代码运行时,测试httpSessionStateAfter抛出

  

System.AggregateException:发生了一个或多个错误   ----> System.NullReferenceException:未将对象引用设置为对象的实例。

只有在我运行单元测试时才会发生这种情况。当应用程序运行此工作正常。 我正在使用Nunit 2.6.3和reshaper测试运行器。

 var httpSessionStateBefour = System.Web.HttpContext.Current.Session;
 var Person= await Db.Persons.FirstOrDefaultAsync();
 var httpSessionStateAfter = System.Web.HttpContext.Current.Session;

如何解决这个问题?

这是我嘲笑HttpContext的方式

 HttpContext.Current = Fakes.FakeHttpContext();            
 HttpContext.Current.Session.Add("IsUserSiteAdmin", true);
 HttpContext.Current.Session.Add("CurrentSite", null);

public static class Fakes
{
    public static HttpContext FakeHttpContext()
    {
        var httpRequest = new HttpRequest("", "http://stackoverflow/", "");
        var stringWriter = new StringWriter();
        var httpResponce = new HttpResponse(stringWriter);
        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});

        return httpContext;
    }
}

3 个答案:

答案 0 :(得分:11)

首先,我建议您尽可能地将代码与HttpContext.Current隔离开来;这不仅会使您的代码更易于测试,而且还有助于您为ASP.NET vNext做好准备,它更像OWIN(没有HttpContext.Current)。

但是,这可能需要进行大量更改,您可能尚未做好准备。要正确模拟HttpContext.Current,您需要了解其工作原理。

HttpContext.Current是一个由ASP.NET SynchronizationContext控制的每线程变量。此SynchronizationContext是“请求上下文”,表示当前请求;它是由ASP.NET在新请求进入时创建的。如果您对更多详细信息感兴趣,我会有MSDN article on SynchronizationContext

正如我在async intro blog post中解释的那样,当您await Task时,默认情况下它将捕获当前的“上下文”并使用它来恢复async方法。在ASP.NET请求上下文中运行async方法时,await捕获的“上下文”是ASP.NET SynchronizationContext。当async方法恢复时(可能在不同的线程上),ASP.NET SynchronizationContext将在恢复HttpContext.Current方法之前设置async。这就是async / await在ASP.NET主机中的工作方式。

现在,当您在单元测试中运行相同的代码时,行为会有所不同。具体来说,没有ASP.NET SynchronizationContext来设置HttpContext.Current。我假设您的单元测试方法返回Task,在这种情况下,NUnit根本不提供SynchronizationContext。因此,当async方法恢复时(可能在不同的线程上),其HttpContext.Current可能不是同一个。

有几种不同的方法可以解决这个问题。一种选择是编写自己的SynchronizationContext来保留HttpContext.Current,就像ASP.NET一样。更简单(但效率更低)的选项是使用a SynchronizationContext that I wrote called AsyncContext,这可确保async方法将在同一个线程上恢复。您应该能够安装my AsyncEx library from NuGet,然后将单元测试方法包装在AsyncContext.Run的调用中。请注意,单元测试方法现在是同步的:

[Test]
public void MyTest()
{
  AsyncContext.Run(async () =>
  {
    // Test logic goes here: set HttpContext.Current, etc.
  });
}

答案 1 :(得分:8)

HttpContext.Current被认为是一个非常可怕的财产;它不会在ASP.NET主页之外表现出来。 修复代码的最佳方法是停止查看此属性并找到将其与您正在测试的代码隔离的方法。例如,您可以创建一个代表当前会话的界面&#39 ; s数据并将该接口公开给您正在测试的组件,其实现需要HTTP上下文。


根本问题与HttpContext.Current的工作方式有关。这个属性是神奇的"在ASP.NET框架中,它对于请求 - 响应操作是唯一的,但是在执行需要时在线程之间跳转 - 它在线程之间有选择地共享。

当您在ASP.NET处理管道之外使用HttpContext.Current时,魔力消失了。当您使用异步编程样式切换线程时,属性为null后继续。

如果您绝对无法更改代码以删除HttpContext.Current上的硬依赖关系,则可以通过利用本地上下文来欺骗此测试:当您声明延续时,本地范围内的所有变量都可用于上下文继续。

// Bring the current value into local scope.
var context = System.Web.HttpContext.Current;

var httpSessionStateBefore = context.Session;
var person = await Db.Persons.FirstOrDefaultAsync();
var httpSessionStateAfter = context.Session;

要明确的是,适用于您当前的方案。如果你在另一个范围内引入await,代码将突然再次中断;这是一个快速而肮脏的答案,我鼓励你忽略并寻求更强大的解决方案。

答案 2 :(得分:2)

当我的代码中出现问题时,我遇到了这个问题... Async MVC Action 中的 HTTPContext.Current在Await 之后为空。我在这里发帖是因为像我这样的人可能会来这里。

我的一般建议是将会话中的任何内容添加到局部变量中,就像上面讨论的其他内容一样,但不要担心保留上下文,而只是担心抓住你想要的实际项目。

public async Task<ActionResult> SomeAction(SomeModel model)
{
int id = (int)HttpContext.Current.Session["Id"];

/* Session Exists Here */

var somethingElseAsyncModel = await GetSomethingElseAsync(model);

/* Session is Null Here */

// Do something with id, thanks to the fact we got it when we could
}