【问题标题】:Unit Testing MVC 5 Controllers that use SiteMaps.Current.CurrentNode单元测试使用 SiteMaps.Current.CurrentNode 的 MVC 5 控制器
【发布时间】:2015-12-18 11:38:47
【问题描述】:

我们有一些控制器操作可以将面包屑节点的标题更改为用户正在查看的项目的值,例如

    [MvcSiteMapNode(Title = "{0}", ParentKey = "Maintenance-Settings-Index", Key = "Maintenance-Settings-Details", PreservedRouteParameters = "id", Attributes = "{\"visibility\":\"SiteMapPathHelper,!*\"}")]
    public async Task<ActionResult> Details(int id)
    {
        var model = await GetSetting(id);
        var node = SiteMaps.Current.CurrentNode;
        if (node != null)
        {
            node.Title = string.Format("{0}", model.Name);
        }
        return View(model);
    }

这在正常查看网站时可以正常工作,并且可以按照我们想要的方式运行..

但是...当尝试使用 Moq 和 FluentMVCTesting 对控制器操作进行单元测试时,我们遇到了错误。

http://www.shiningtreasures.com/post/2013/08/14/mvcsitemapprovider-4-unit-testing-with-the-sitemaps-static-methods 我们添加了SiteMaps.Loader = new Mock&lt;ISiteMapLoader&gt;().Object; 例如

创建控制器上下文

    private static ControllerContext FakeControllerContext(RouteData routeData)
    {
        var context = new Mock<HttpContextBase>();
        var request = new Mock<HttpRequestBase>();
        var response = new Mock<HttpResponseBase>();
        var session = new MockHttpSession();
        var server = new Mock<HttpServerUtilityBase>();
        context.Setup(ctx => ctx.Request).Returns(request.Object);
        context.Setup(ctx => ctx.Response).Returns(response.Object);
        context.Setup(ctx => ctx.Session).Returns(session);
        context.Setup(ctx => ctx.Server).Returns(server.Object);

        var controllerContext = new ControllerContext(context.Object, routeData ?? new RouteData(), new Mock<ControllerBase>().Object);
        return controllerContext;
    }

为每个测试初始化​​控制器

 [TestInitialize]
    public void Initialize()
    {
        var routeData = new RouteData();

        _controller = new DepartmentSettingsController
        {
            ControllerContext = FakeControllerContext(routeData)
        };
    }

然后是测试本身

[TestMethod]
    public void Details()
    {
        SiteMaps.Loader = new Mock<ISiteMapLoader>().Object;
        _controller.WithCallTo(c => c.Details(_model.Id)).ShouldRenderDefaultView()
            .WithModel<SettingViewModel>(m => m.Name == _model.Name);
    }

我们得到以下错误 System.NullReferenceException: Object reference not set to an instance of an object. 指的是var node = SiteMaps.Current.CurrentNode;

然后我们添加另一个测试

 [TestMethod]
    public void Edit()
    {
        SiteMaps.Loader = new Mock<ISiteMapLoader>().Object;
        _controller.WithCallTo(c => c.Edit(_model.Id)).ShouldRenderDefaultView()
            .WithModel<SettingViewModel>(m => m.Name == _model.Name);
    }

并得到MvcSiteMapProvider.MvcSiteMapException:站点地图加载器只能在Global.asax的Application_Start事件中设置,不得再次设置。如果您使用的是外部依赖注入容器,请将 web.config 文件的 AppSettings 部分中的“MvcSiteMapProvider_UseExternalDIContainer”设置为“true”。 在 MvcSiteMapProvider.SiteMaps.set_Loader(ISiteMapLoader 值)

然后将SiteMaps.Loader = new Mock&lt;ISiteMapLoader&gt;().Object; 移动到测试初始化​​例如

    [TestInitialize]
    public void Initialize()
    {
        var routeData = new RouteData();
        _controller = new DepartmentSettingsController
        {
            ControllerContext = FakeControllerContext(routeData)
        };
        SiteMaps.Loader = new Mock<ISiteMapLoader>().Object;
    }

我们得到同样的错误 MvcSiteMapProvider.MvcSiteMapException: 站点地图加载器只能在 Global.asax 的 Application_Start 事件中设置,不得再次设置。如果您使用的是外部依赖注入容器,请将 web.config 文件的 AppSettings 部分中的“MvcSiteMapProvider_UseExternalDIContainer”设置为“true”。 在 MvcSiteMapProvider.SiteMaps.set_Loader(ISiteMapLoader 值)

问题 - 当您测试多个操作时,SiteMaps.Loader = new Mock&lt;ISiteMapLoader&gt;().Object; 在单元测试中的最佳位置在哪里

问题 - 使用静态 var node = SiteMaps.Current.CurrentNode; 是进入控制器的最佳方式,还是有更好的方式(我们使用 Unity)

感谢您的帮助

【问题讨论】:

    标签: unit-testing moq mvcsitemapprovider asp.net-mvc-sitemap


    【解决方案1】:

    另类

    对于这个特定的用例,您根本不需要访问静态 SiteMaps 类。 MvcSiteMapProvider.Web.Mvc.Filters 命名空间中有一个SiteMapTitle 操作过滤器属性,可用于根据您的模型设置标题。

    [MvcSiteMapNode(Title = "{0}", ParentKey = "Maintenance-Settings-Index", Key = "Maintenance-Settings-Details", PreservedRouteParameters = "id", Attributes = "{\"visibility\":\"SiteMapPathHelper,!*\"}")]
    [SiteMapTitle("Name")]
    public async Task<ActionResult> Details(int id)
    {
        var model = await GetSetting(id);
        return View(model);
    }
    

    模拟 ISiteMapLoader

    至于设置ISiteMapLoader,我现在看到有问题,因为它是静态的。这意味着它将在单元测试框架的运行器进程的整个生命周期中存在,而不管设置/拆除了多少测试。理想情况下,有一种方法可以读取 Loader 属性(或其他类似的检查)以查看它是否已被填充,如果是则跳过该步骤,但不幸的是,情况并非如此。

    因此,下一个最好的办法是创建一个静态帮助器类来跟踪 ISiteMapLoader 是否已加载,如果已加载则跳过设置操作。

    public class SiteMapLoaderHelper
    {
        private static ISiteMapLoader loader;
    
        public static void MockSiteMapLoader()
        {
            // If the loader already exists, skip setting up.
            if (loader == null)
            {
                loader = new Mock<ISiteMapLoader>().Object;
                SiteMaps.Loader = loader;
            }
        }
    }
    

    用法

     [TestInitialize]
     public void Initialize()
     {
         var routeData = new RouteData();
    
         _controller = new DepartmentSettingsController
         {
             ControllerContext = FakeControllerContext(routeData)
         };
    
         // Setup SiteMapLoader Mock
         SiteMapLoaderHelper.MockSiteMapLoader();
     }
    

    当然,缺点是你的 mock 并不孤立于特定的单元测试,所以你对整个测试套件的所有 mock 必须在一个地方完成(假设你需要 mock ISiteMapLoader 的其他成员及其依赖项)。

    另一种可能的选择

    如果您愿意更改测试框架,还有另一种可能性。您可以将测试设置为在各自的 AppDomain 中运行,这应该允许为每个测试卸载静态 ISiteMapLoader 实例。

    我在this question 中发现有一个NUnit.AppDomain 包可用于执行此操作。

    也有人指出XUnit 自动在单独的 AppDomain 中运行单元测试,无需额外配置。

    如果无法更改单元测试框架,您可以通过将与静态成员交互的每个单元测试放入单独的程序集来解决此问题。

    MsTest 为每个测试程序集创建一个应用域,除非您使用 noisolation,在这种情况下没有 AppDomain 隔离。

    参考:MSTest & AppDomains

    【讨论】:

    • 非常感谢您的回复。我们还使用(在某些地方)var node = SiteMaps.Current.CurrentNode; if (node != null) { node.Title = string.Format("Edit {0}", widget.Name); if (node.ParentNode != null) { node.ParentNode.Title = string.Format("{0}", widget.Supplier); } },因此将使用 Mocking ISiteMapLoader 选项
    • 查看我的编辑 - 我认为“另一种可能的替代方案”部分是最好的选择,因为它意味着您可以为每个测试单独设置单元测试设置。
    • 好的,谢谢。我们目前使用 MSTest 和 nCurnch,所以所有的测试都是用那个写的......
    • 在使用SiteMapLoaderHelper.MockSiteMapLoader(); 时,var node = SiteMaps.Current.CurrentNode; 上仍然出现空引用异常
    • 那是因为您实际上并没有模拟 ISiteMapLoader.GetSiteMap() 方法(它驱动 SiteMaps.Current 属性)或 ISiteMap.CurrentNode 属性。
    猜你喜欢
    • 1970-01-01
    • 2011-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-14
    • 1970-01-01
    相关资源
    最近更新 更多