41

考虑 .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 解决方案的一部分。单元测试看起来像这样,但我缺少最小起订量代码。

//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()
  • 您对如何改进此代码/架构有任何建议吗?
4

7 回答 7

46

由于这个确切的原因,Webforms 众所周知是不可测试的——许多代码可以依赖于 asp.net 管道中的静态类。

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

HttpContextWrapper驻留在System.Web.Abstractions.Net 3.5 中。它是HttpContext类的包装器和 extends HttpContextBase,你可以HttpContextWrapper像这样构造一个:

var wrapper = new HttpContextWrapper(HttpContext.Current);

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

var mockContext = new Mock<HttpContextBase>();

有了这个,您可以调用GetSecurityContextUserName(mockContext.Object),并且您的应用程序与静态 WebForms HttpContext 的耦合要少得多。如果您要进行大量依赖于模拟上下文的测试,我强烈建议您查看 Scott Hanselman 的 MvcMockHelpers 类,它有一个用于 Moq 的版本。它可以方便地处理许多必要的设置。尽管有这个名字,你不需要用 MVC 来做——当我可以重构它们以使用HttpContextBase.

于 2009-07-31T18:55:45.440 回答
3
[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"));
于 2014-04-16T00:36:58.723 回答
3

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

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

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

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。

于 2012-01-30T22:34:25.740 回答
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。

于 2012-03-13T12:52:20.377 回答
1

在 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);
}
于 2021-04-22T14:00:33.123 回答
0

如果您正在使用 CLR 安全模型(如我们所做的那样),那么如果您想允许测试,则需要使用一些抽象函数来获取和设置当前主体,并在获取或设置主体时使用这些函数。这样做可以让您在任何相关的地方获取/设置主体(通常在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.

public class MyCustomPrincipal : IPrincipal
{
    public MyCustomPrincipal Current { get; }
    public bool HasCurrent { get; }
    public void SetAsCurrent();
}
于 2009-07-31T19:14:34.933 回答
0

这与使用 Moq 对您需要的内容进行单元测试并没有真正的关系。

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

通过这种方式,您可以避免弄乱 Mock 框架,尝试模拟 HttpRequests 等……尽管通常它可能仍然是必要的。

于 2009-07-31T19:20:36.763 回答