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

Unit Testing MVC 5 Controllers that use SiteMaps.Current.CurrentNode

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

    [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<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;
    }

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

 [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 引用未设置为 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<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)

感谢您的帮助

备选

对于这个特定的用例,您根本不需要访问静态 SiteMaps class。 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 的设置,我现在发现存在问题,因为它是静态的。这意味着无论有多少测试 setup/torn 失败,它都将存在于单元测试框架的 运行ner 过程的整个生命周期中。理想情况下,有一种方法可以读取 Loader 属性 (或其他类似检查)以查看它是否已被填充,如果已填充则跳过该步骤,但不幸的是,事实并非如此案例

所以,下一个最好的办法是制作一个静态助手 class 来跟踪 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 都必须在一个地方完成(假设你需要模拟 ISiteMapLoader 及其依赖项)。

另一种可能的选择

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

我在 this question that there is an NUnit.AppDomain 包中发现可以用来做这个。

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

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

MsTest creates one-app domain per Test assembly, unless you are using noisolation, in which case there is no AppDomain Isolation.

参考:MSTest & AppDomains