Moq:单元测试依赖于HttpContext的方法

时间:2009-07-31 18:43:42

标签: c# unit-testing mocking moq

考虑.NET程序集中的方法:

public static string GetSecurityContextUserName()
{             
 //extract the username from request              
 string sUser = HttpContext.Current.User.Identity.Name;
 //everything after the domain     
 sUser = sUser.Substring(sUser.IndexOf("\\") + 1).ToLower();

 return sUser;      
}

我想使用Moq框架从单元测试中调用此方法。该程序集是webforms解决方案的一部分。单元测试看起来像这样,但我错过了Moq代码。

//arrange 
 string ADAccount = "BUGSBUNNY";
 string fullADName = "LOONEYTUNES\BUGSBUNNY"; 

 //act    
 //need to mock up the HttpContext here somehow -- using Moq.
 string foundUserName = MyIdentityBL.GetSecurityContextUserName();

 //assert
 Assert.AreEqual(foundUserName, ADAccount, true, "Should have been the same User Identity.");

问题

  • 如何使用Moq来安排假冒的HttpContext对象,其值如'MyDomain \ MyUser'?
  • 如何将假冒与我的通话关联到MyIdentityBL.GetSecurityContextUserName()的静态方法?
  • 您对如何改进此代码/架构有任何建议吗?

7 个答案:

答案 0 :(得分:42)

出于这个原因,Webforms众所周知是不可测试的 - 很多代码都可以依赖asp.net管道中的静态类。

为了使用Moq进行测试,您需要重构GetSecurityContextUserName()方法以使用HttpContextBase对象的依赖注入。

HttpContextWrapper位于System.Web.Abstractions,随附.Net 3.5。它是HttpContext类的包装器,并扩展为HttpContextBase,您可以像这样构建HttpContextWrapper

var wrapper = new HttpContextWrapper(HttpContext.Current);

更好的是,您可以使用Moq模拟HttpContextBase并设置它的期望。包括登录用户等

var mockContext = new Mock<HttpContextBase>();

有了这个,你可以调用GetSecurityContextUserName(mockContext.Object),你的应用程序与静态WebForms HttpContext的耦合要少得多。如果你要做很多依赖于模拟上下文的测试,我强烈建议taking a look at Scott Hanselman's MvcMockHelpers class,它有一个用于Moq的版本。它可以方便地处理许多必要的设置。尽管有这个名字,但你不需要使用MVC - 当我可以重构它们以使用HttpContextBase时,我成功地使用它与webforms应用程序。

答案 1 :(得分:3)

一般来说,对于ASP.NET单元测试,不应该访问HttpContext.Current,而应该有一个HttpContextBase类型的属性,其值由依赖注入设置(例如在Womp提供的答案中)。

但是,为了测试安全相关的函数,我建议使用Thread.CurrentThread.Principal(而不是HttpContext.Current.User)。使用Thread.CurrentThread的优点是也可以在Web上下文之外重用(并且在Web上下文中的工作方式相同,因为ASP.NET框架始终将两个值设置为相同)。

然后测试Thread.CurrentThread.Principal我通常使用一个范围类,将Thread.CurrentThread设置为测试值,然后重置dispose:

using (new UserResetScope("LOONEYTUNES\BUGSBUNNY")) {
    // Put test here -- CurrentThread.Principal is reset when PrincipalScope is disposed
}

这非常适合标准的.NET安全组件 - 其中组件具有已知的接口(IPrincipal)和位置(Thread.CurrentThread.Principal) - 并且可以使用正确使用/检查Thread的任何代码。 CurrentThread.Principal。

基本范围类将类似于以下内容(根据需要进行调整,例如添加角色):

class UserResetScope : IDisposable {
    private IPrincipal originalUser;
    public UserResetScope(string newUserName) {
        originalUser = Thread.CurrentPrincipal;
        var newUser = new GenericPrincipal(new GenericIdentity(newUserName), new string[0]);
        Thread.CurrentPrincipal = newUser;
    }
    public IPrincipal OriginalUser { get { return this.originalUser; } }
    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            Thread.CurrentPrincipal = originalUser;
        }
    }
}

另一种方法是,编写您的应用程序以使用注入的安全性详细信息,而不是使用标准安全组件位置。使用GetCurrentUser()方法或类似方法添加ISecurityContext属性,然后在整个应用程序中一致地使用它 - 但如果您要在Web应用程序的上下文中执行此操作,那么您也可以使用预先构建的注入上下文,HttpContextBase。

答案 2 :(得分:1)

看看这个 http://haacked.com/archive/2007/06/19/unit-tests-web-code-without-a-web-server-using-httpsimulator.aspx

使用httpSimulator类,您将能够将HttpContext传递给处理程序

HttpSimulator sim = new HttpSimulator("/", @"C:\intepub\?")
.SimulateRequest(new Uri("http://localhost:54331/FileHandler.ashx?
ticket=" + myticket + "&fileName=" + path));

FileHandler fh = new FileHandler();
fh.ProcessRequest(HttpContext.Current);

HttpSimulator实现了获取HttpContext实例所需的内容。所以你不需要在这里使用Moq。

答案 3 :(得分:1)

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

你也可以像下面那样进行搜索

var controllerContext = new Mock<ControllerContext>();
      controllerContext.SetupGet(p => p.HttpContext.Session["User"]).Returns(TestGetUser);
      controllerContext.SetupGet(p => p.HttpContext.Request.Url).Returns(new Uri("http://web1.ml.loc"));

答案 4 :(得分:0)

如果你正在使用CLR安全模型(就像我们一样)那么你需要使用一些抽象函数来获取和设置当前主体,如果你想允许测试,并在获取或设置主体时使用它们。这样做可以让你在相关的任何地方获取/设置主体(通常在Web上的HttpContext上,以及在其他地方的当前线程上,比如单元测试)。这看起来像是:

public static IPrincipal GetCurrentPrincipal()
{
    return HttpContext.Current != null ?
        HttpContext.Current.User :
        Thread.CurrentThread.Principal;
}

public static void SetCurrentPrincipal(IPrincipal principal)
{
     if (HttpContext.Current != null) HttpContext.Current.User = principal'
     Thread.CurrentThread.Principal = principal;
}

如果您使用自定义主体,那么这些可以很好地集成到其界面中,例如,Current下方会调用GetCurrentPrincipalSetAsCurrent会调用SetCurrentPrincipal。< / p>

public class MyCustomPrincipal : IPrincipal
{
    public MyCustomPrincipal Current { get; }
    public bool HasCurrent { get; }
    public void SetAsCurrent();
}

答案 5 :(得分:0)

这与使用Moq进行单元测试所需的功能并不相关。

通常我们在工作中都有一个分层架构,其中表示层上的代码实际上只是用于安排在UI上显示的内容。这种代码不包含在单元测试中。所有其余的逻辑都驻留在业务层上,业务层不必依赖于表示层(即特定于UI的引用,如HttpContext),因为UI也可能是WinForms应用程序,不一定是Web应用程序

通过这种方式,你可以避免乱搞Mock框架,试图模拟HttpRequests等......尽管通常它仍然是必要的。

答案 6 :(得分:0)

在 ASP.NET MVC Core 中,我使用以下代码来测试依赖于 HttpContext 的控制器:

var controller = new HomeController();
controller.ControllerContext.HttpContext = new DefaultHttpContext();

这是一个示例单元测试:

[Test]
public void Test_HomeController_Index()
{
    // Arrange
    var controller = new HomeController();
    controller.ControllerContext.HttpContext = new DefaultHttpContext();

    // Act
    var result = controller.Index();

    // Assert
    var viewResult = result as ViewResult;
    Assert.IsNotNull(viewResult);
}