【问题标题】:Moq: unit testing a method relying on HttpContextMoq:对依赖 HttpContext 的方法进行单元测试
【发布时间】:2009-07-31 18:43:42
【问题描述】:

考虑 .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() 处将这个虚假与我对静态方法的调用联系起来?
  • 您对如何改进此代码/架构有任何建议吗?

【问题讨论】:

    标签: c# unit-testing mocking moq


    【解决方案1】:

    众所周知,Webforms 正是因为这个原因而无法测试 - 很多代码都依赖于 asp.net 管道中的静态类。

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

    HttpContextWrapper 位于System.Web.Abstractions,随 .Net 3.5 一起提供。它是HttpContext 类的包装器,并扩展了HttpContextBase,您可以像这样构造HttpContextWrapper:

    var wrapper = new HttpContextWrapper(HttpContext.Current);
    

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

    var mockContext = new Mock<HttpContextBase>();
    

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

    【讨论】:

    • 你不能使用 new HttpContextBase() 因为它是一个抽象类(顾名思义)。您需要改用 new HttpContextWrapper()。
    • 谢谢...当我回答这个问题时,我猜我的脑子已经提前了;)
    【解决方案2】:

    一般来说,对于 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。

    【讨论】:

      【解决方案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"));
      

      【讨论】:

        【解决方案4】:

        看看这个 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。

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

            如果您使用 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 将调用GetCurrentPrincipal,SetAsCurrent 将调用SetCurrentPrincipal。

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

            【讨论】:

            • 这段代码是不必要的,因为如果你反思 ASP.NET 代码,你会发现 HttpContext.Current.User 和 Thread.CurrentThread.Principal 总是设置为相同的值,所以你只需要检查 Thread.CurrentThread。这甚至可以在上面的代码中看到,其中 SetCurrentPrincipal 始终设置 Thread.CurrentPrincipal,并更新 HttpContext(如果存在)相同;它们在任何时候都没有不同的价值观。 (换句话说,总是使用 Thread.CurrentThread。)
            • @Sly - 它们是相同的,除非它们不同。 hanselman.com/blog/…
            【解决方案7】:

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

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2012-06-01
              • 1970-01-01
              • 2021-10-15
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多