【问题标题】:MVC3 HttpContext unit testing / mocking optionsMVC3 HttpContext 单元测试/模拟选项
【发布时间】:2011-10-31 16:56:29
【问题描述】:

所以这确实适用于几个不同的类,如 HttpContext、ConfigurationManager 等。有几种不同的方法来处理这个,我一直使用包装类来处理这些东西,但我想看看最常见的社区实践是什么是……

  1. 包装类 - 例如我会有一个 HttpContextService,我通过构造函数传入它,它通过平面方法调用公开所有相同的功能。
  2. 包装类(第 2 部分) - 例如我会有特定的服务类,比如 MembershipService,它在幕后利用 HttpContext。功能与 1 相同,但命名方案/使用模式略有不同,因为您通过特定服务而不是一个整体包装器公开特定功能。缺点是需要注入的服务类的数量增加了,但是当您不需要整体包装器的所有功能时,您可以获得一些模块化。
  3. ActionFilters 和参数 - 使用 ActionFilter 自动传递每个函数所需的某些值。仅限 MVC,并将您限制为控制器方法,而 1 和 2 可以在整个项目中通用,甚至可以与此选项结合使用。
  4. 直接模拟 HttpContextBase 并设置 ControllerContext - 有几种模拟框架扩展方法可以帮助解决这个问题,但本质上需要您根据需要直接设置。不需要抽象,这很好,也可以在非控制器测试中重用。不过,ConfigurationManager 和其他静态方法调用仍然存在问题,因此您最终可能会以 ANYWAY 方式注入,但会以其他方式访问 HttpContext。

现在我在做 1 号,所以我有一个 HttpContextService 和一个 ConfigurationManagerService 等,然后我会注入它们,尽管我将来倾向于 2。 3 对我的口味来说似乎有点太乱了,但我可以看到对控制器方法的吸引力,并且对于也使用这些静态类的其他代码区域需要一个完全独立的解决方案,这对我来说是一种糟糕的选择。 .. 4 对我来说仍然很有趣,因为它在基本功能方面似乎是最“自然”的,并且利用了 MVC 的内置方法。

那么这里流行的最佳实践是什么?人们在野外看到和使用什么?

【问题讨论】:

    标签: asp.net-mvc unit-testing mocking httpcontext


    【解决方案1】:

    已经有 HttpContext、HttpRequest、HttpResponse 等的“包装”类。MVC 框架使用这些类,您可以通过控制器上下文将它们的模拟提供给控制器。您不需要模拟控制器上下文,因为您可以简单地使用适当的值创建一个。我发现唯一难以模拟的是帮助程序 UrlHelper 和 HtmlHelper。那些有一些相对较深的依赖关系。您可以通过某种合理的方式伪造它们,如下所示的 UrlHelper。

     var httpContext = MockRepository.GenerateMock<HttpContextBase>();
     var routeData = new RoutedData();
    
     var controller = new HomeController();
     controller.ControllerContext = new ControllerContext( httpContext, routeData, controller );
     controller.Url = UrlHelperFactory.CreateUrlHelper( httpContext, routeDate );
    

    在哪里

     public static class UrlHelperFactory
     {
        public static UrlHelper CreateUrlHelper( HttpContextBase httpContext, RouteData routeData )
        {
            return CreateUrlHelper( httpContext, routeData, "/" );
        }
    
        public static UrlHelper CreateUrlHelper( HttpContextBase httpContext, RouteData routeData, string url )
        {
            string urlString = string.Format( "http://localhost/{0}/{1}/{2}", routeData.Values["controller"], routeData.Values["action"], routeData.Values["id"] ).TrimEnd( '/' );
    
            var uri = new Uri( urlString );
    
            if (httpContext.Request == null)
            {
                httpContext.Stub( c => c.Request ).Return( MockRepository.GenerateStub<HttpRequestBase>() ).Repeat.Any();
            }
    
            httpContext.Request.Stub( r => r.Url ).Return( uri ).Repeat.Any();
            httpContext.Request.Stub( r => r.ApplicationPath ).Return( "/" ).Repeat.Any();
    
            if (httpContext.Response == null)
            {
                httpContext.Stub( c => c.Response ).Return( MockRepository.GenerateStub<HttpResponseBase>() ).Repeat.Any();
            }
            if (url != "/")
            {
                url = url.TrimEnd( '/' );
            }
    
            httpContext.Response.Stub( r => r.ApplyAppPathModifier( Arg<string>.Is.Anything ) ).Return( url ).Repeat.Any();
    
            return new UrlHelper( CreateRequestContext( httpContext, routeData ), GetRoutes() );
        }
    
        public static RequestContext CreateRequestContext( HttpContextBase httpContext, RouteData routeData )
        {
            return new RequestContext( httpContext, routeData );
        }
    
        // repeat your route definitions here!!!
        public static RouteCollection GetRoutes()
        {
            RouteCollection routes = new RouteCollection();
            routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );
    
    
            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "home", action = "index", id = "" }  // Parameter defaults
            );
    
            return routes;
        }
    }
    

    【讨论】:

    • 对,这将是选项 4,这仅适用于 HttpContextBase。正如您所展示的,我可以抽象出此测试设置代码,但这不包括其他静态类,如 ConfigurationManager 等,因此您最终会得到相同问题的两种解决方案,而选项 1 或 2 将它们统一起来。正如我所说,它们是两种不同的解决方案,我假设您更喜欢在这里使用选项 4...
    • 除非您不提供模拟控制器上下文,而是提供嵌入了相关模拟的实际控制器上下文。我确实对 FormsAuthentication、MembershipProvider 等带有适当包装器的东西使用构造函数注入,尽管我只在我的 AccountController 上使用这些。对于其他我通过 HttpContext 模拟的东西,比如请求、响应、用户、会话等。
    • 噢!我的坏,在原帖中的错误。这就是我想说的。现在修好了。关于您为什么喜欢这种方法而不是将对象作为一个整体(选项 1)或部分按角色(选项 2)封装的任何评论,或者我是否对“注入的依赖项数量快速增长”感到非常满意?跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-13
    • 2012-06-29
    • 1970-01-01
    • 2017-05-14
    相关资源
    最近更新 更多