单元测试MVC 5使用SiteMaps.Current.CurrentNode的控制器

时间:2015-12-18 11:38:48

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

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

    [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和FluentMVCTest来对控制器操作进行单元测试时,我们会遇到错误。

http://www.shiningtreasures.com/post/2013/08/14/mvcsitemapprovider-4-unit-testing-with-the-sitemaps-static-methods我们添加了SiteMaps.Loader = new Mock<ISiteMapLoader>().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;
    }

为每个测试初始化​​Controller

 [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:对象引用未设置为对象的实例。引用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<ISiteMapLoader>().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<ISiteMapLoader>().Object;的最佳位置

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

感谢您的帮助

1 个答案:

答案 0 :(得分: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属性(或其他类似的检查)以查看它是否已经填充,然后跳过该步骤(如果是,但不幸的是情况并非如此)。 / p>

因此,下一个最好的方法是创建一个静态助手类来跟踪是否已加载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();
 }

当然,缺点是你的模拟不是与特定的单元测试隔离,所以你对整个测试套件的所有模拟必须在一个地方完成(假设你需要模拟{{1的其他成员)和它的依赖关系)。

另一种可能的选择

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

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

有人还指出ISiteMapLoader会在单独的AppDomain中自动运行单元测试而无需额外配置。

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

  

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

参考:MSTest & AppDomains