在测试初始化方法中模拟HttpContext.Current

204

我正在尝试为我构建的ASP.NET MVC应用程序添加单元测试。在我的单元测试中,我使用以下代码:

[TestMethod]
public void IndexAction_Should_Return_View() {
    var controller = new MembershipController();
    controller.SetFakeControllerContext("TestUser");

    ...
}

使用以下助手来模拟控制器上下文:

public static class FakeControllerContext {
    public static HttpContextBase FakeHttpContext(string username) {
        var context = new Mock<HttpContextBase>();

        context.SetupGet(ctx => ctx.Request.IsAuthenticated).Returns(!string.IsNullOrEmpty(username));

        if (!string.IsNullOrEmpty(username))
            context.SetupGet(ctx => ctx.User.Identity).Returns(FakeIdentity.CreateIdentity(username));

        return context.Object;
    }

    public static void SetFakeControllerContext(this Controller controller, string username = null) {
        var httpContext = FakeHttpContext(username);
        var context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);
        controller.ControllerContext = context;
    }
}

这个测试类继承了一个基类,该基类具有以下内容:

[TestInitialize]
public void Init() {
    ...
}

在这个方法内部,它调用了一个我无法控制的库,该库试图运行以下代码:

HttpContext.Current.User.Identity.IsAuthenticated

现在您可能已经看到问题了。我已经针对控制器设置了假的HttpContext,但没有在这个基础的Init方法中设置。单元测试/模拟对我来说非常新,所以我想确保我做对了。请问我正确的方法是如何模拟HttpContext,以便它在我的控制器和在我的Init方法中调用的任何库之间共享。

4个回答

406

HttpContext.Current 返回 System.Web.HttpContext 的一个实例,它没有扩展 System.Web.HttpContextBase。后来添加了 HttpContextBase 以解决难以模拟 HttpContext 的问题。这两个类基本上是不相关的(HttpContextWrapper 用作它们之间的适配器)。

幸运的是,HttpContext 本身可以被伪造,你可以替换 IPrincipal(用户)和 IIdentity

以下代码可在控制台应用程序中正常运行:

HttpContext.Current = new HttpContext(
    new HttpRequest("", "http://tempuri.org", ""),
    new HttpResponse(new StringWriter())
    );

// User is logged in
HttpContext.Current.User = new GenericPrincipal(
    new GenericIdentity("username"),
    new string[0]
    );

// User is logged out
HttpContext.Current.User = new GenericPrincipal(
    new GenericIdentity(String.Empty),
    new string[0]
    );

5
如果你将一个空字符串传递给GenericIdentity构造函数,IsAuthenticated会返回false。 - Richard Szalay
2
这个能用来模拟HttpContext中的缓存吗? - DevDave
4
MVC使用HttpContextBase,可以进行模拟。如果您正在使用MVC,则无需使用我发布的解决方法。如果您坚持使用它,那么在甚至_创建_控制器之前,您可能需要运行我发布的代码。 - Richard Szalay
如何在 HttpRequest 中设置 IP / 浏览器? - FrenkyB
在这种方法下,我们如何存根Session属性? - Vin Shahrdar
显示剩余3条评论

42

以下测试初始化也可以完成工作。

[TestInitialize]
public void TestInit()
{
  HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));
  YourControllerToBeTestedController = GetYourToBeTestedController();
}

我无法在解决方案中的单独测试项目中获取HTTPContext的可寻址性。您能通过继承控制器来获取它吗? - JWP
1
你的测试项目中有引用到 System.Web 吗? - PUG
是的,但我的项目是MVC项目,MVC版本的System.Web可能只包含该命名空间的子集,这种情况有可能发生吗? - JWP
2
@user1522548(你应该创建一个账户)程序集System.Web.dll,v4.0.0.0肯定有HTTPContext,我刚刚检查了我的源代码。 - PUG
我的错误,我在文件中引用了 System.Web.MVC,而不是 System.Web。感谢你的帮助。 - JWP

10

我知道这是一个较老的话题,但在单元测试中Mock一个MVC应用程序是我们非常经常做的事情。

我想补充一下我的经验,即在升级到Visual Studio 2013后使用Moq 4 Mock一个MVC 3应用程序。在调试模式下,所有的单元测试都不起作用,并且当尝试窥视变量时,HttpContext显示“无法评估表达式”。

原来Visual Studio 2013无法评估某些对象。为了再次让模拟的Web应用程序调试起作用,我必须在“工具”=>“选项”=>“调试”=>“通用设置”中勾选“使用托管兼容性模式”。

通常我会像这样做:

public static class FakeHttpContext
{
    public static void SetFakeContext(this Controller controller)
    {

        var httpContext = MakeFakeContext();
        ControllerContext context =
        new ControllerContext(
        new RequestContext(httpContext,
        new RouteData()), controller);
        controller.ControllerContext = context;
    }


    private static HttpContextBase MakeFakeContext()
    {
        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>();

        context.Setup(c=> c.Request).Returns(request.Object);
        context.Setup(c=> c.Response).Returns(response.Object);
        context.Setup(c=> c.Session).Returns(session.Object);
        context.Setup(c=> c.Server).Returns(server.Object);
        context.Setup(c=> c.User).Returns(user.Object);
        user.Setup(c=> c.Identity).Returns(identity.Object);
        identity.Setup(i => i.IsAuthenticated).Returns(true);
        identity.Setup(i => i.Name).Returns("admin");

        return context.Object;
    }


}

像这样启动上下文

FakeHttpContext.SetFakeContext(moController);

直接在控制器中调用该方法

long lReportStatusID = -1;
var result = moController.CancelReport(lReportStatusID);

有没有充分的理由来设置它,而不是接受答案?一开始看起来更加复杂,并且似乎没有提供任何额外的好处。 - kilkfoe
1
它提供了一种更详细/模块化的模拟方法。 - Vincent Buscarello
如果我的测试代码直接引用HttpContext.Current,而没有注入基础依赖项,我该怎么办? - Vin Shahrdar

5

如果您的应用程序进行第三方重定向,则最好按以下方式模拟HttpContext:

HttpWorkerRequest initWorkerRequest = new SimpleWorkerRequest("","","","",new StringWriter(CultureInfo.InvariantCulture));
System.Web.HttpContext.Current = new HttpContext(initWorkerRequest);
System.Web.HttpContext.Current.Request.Browser = new HttpBrowserCapabilities();
System.Web.HttpContext.Current.Request.Browser.Capabilities = new Dictionary<string, string> { { "requiresPostRedirectionHandling", "false" } };

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