Moq:unit testing一个依赖于HttpContext的方法

考虑.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框架的unit testing中调用这个方法。 这个程序集是webforms解决scheme的一部分。 unit testing看起来像这样,但我缺lessMoq代码。

 //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()静态方法相关联?
  • 你有什么build议如何改善这个代码/架构?

出于这个确切原因,Webforms是不可testing的 – 很多代码可以依赖于asp.netpipe道中的静态类。

为了用Moqtesting这个,你需要重构你的GetSecurityContextUserName()方法来使用dependency injection和一个HttpContextBase对象。

HttpContextWrapper驻留在.Net 3.5附带的System.Web.Abstractions 。 它是HttpContext类的包装器,并且扩展了HttpContextBase ,并且可以像这样构造一个HttpContextWrapper

 var wrapper = new HttpContextWrapper(HttpContext.Current); 

更好的是,你可以模拟一个HttpContextBase,并使用Moq设置你的期望值。 包括login的用户等

 var mockContext = new Mock<HttpContextBase>(); 

有了这个,你可以调用GetSecurityContextUserName(mockContext.Object) ,而你的应用程序与静态的WebForms HttpContext很less耦合。 如果你要进行大量依赖于模拟背景的testing,我强烈build议看看Scott Hanselman的MvcMockHelpers类 , 该类有一个与Moq一起使用的版本。 它可以方便地处理很多必要的设置。 而且,尽pipe名字,你不需要与MVC做 – 我成功地使用webforms应用程序时,我可以重构他们使用HttpContextBase

一般来说,对于ASP.NETunit testing,而不是访问HttpContext.Current,你应该有一个types为HttpContextBase的属性,它的值是通过dependency injection设置的(比如在Womp提供的答案中)。

但是,对于testing安全相关的function,我build议使用Thread.CurrentThread.Principal(而不是HttpContext.Current.User)。 使用Thread.CurrentThread的优点是在web上下文之外也是可重用的(并且在web上下文中工作原理相同,因为ASP.NET框架始终将这两个值设置为相同)。

然后testingThread.CurrentThread.Principal我通常使用一个作用域类,将Thread.CurrentThread设置为testing值,然后重置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 。

基本范围类将如下所示(根据需要进行调整,如添加angular色):

 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应用程序的上下文中执行此操作,那么您可以使用预构build的注入上下文HttpContextBase。

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

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

你也可以像下面moq

 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")); 

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

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

在使用Moq进行unit testing时,这并不是真正相关的。

一般来说,我们在工作中都有一个分层架构,表示层上的代码实际上只是用来安排显示在UI上的东西。 这种代码不包含unit testing。 所有其余的逻辑驻留在业务层上,因为UI也可以是WinForms应用程序,并且不一定是Web应用程序,所以不必对表示层具有任何依赖性(即,UI特定引用,如HttpContext) 。

通过这种方式,你可以避免混淆模拟框架,试图模拟HttpRequests等…虽然经常可能仍然是必要的。