unit-testingmoqmvcsitemapproviderasp.net-mvc-sitemap

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


We have some controller actions that change the title of the breadcrumb node to be the value of the item the user is looking at e.g.

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

This works fine when viewing the site normally and behaves how we want it to..

However... When trying to unit test the controller actions using Moq and FluentMVCTesting we are getting errors.

From http://www.shiningtreasures.com/post/2013/08/14/mvcsitemapprovider-4-unit-testing-with-the-sitemaps-static-methods we added the SiteMaps.Loader = new Mock<ISiteMapLoader>().Object; e.g.

Create the Controller Context

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

Intialize the Controller for each test

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

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

Then the test itself

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

We get the following error System.NullReferenceException: Object reference not set to an instance of an object. which refers to var node = SiteMaps.Current.CurrentNode;

Then we add another Test

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

And get MvcSiteMapProvider.MvcSiteMapException: The sitemap loader may only be set in the Application_Start event of Global.asax and must not be set again. Set the 'MvcSiteMapProvider_UseExternalDIContainer' in the AppSettings section of the web.config file to 'true' if you are using an external dependency injection container. at MvcSiteMapProvider.SiteMaps.set_Loader(ISiteMapLoader value)

Then moving SiteMaps.Loader = new Mock<ISiteMapLoader>().Object; into the test intialize e.g.

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

we get the same error MvcSiteMapProvider.MvcSiteMapException: The sitemap loader may only be set in the Application_Start event of Global.asax and must not be set again. Set the 'MvcSiteMapProvider_UseExternalDIContainer' in the AppSettings section of the web.config file to 'true' if you are using an external dependency injection container. at MvcSiteMapProvider.SiteMaps.set_Loader(ISiteMapLoader value)

Question - Where is the best place for SiteMaps.Loader = new Mock<ISiteMapLoader>().Object; in the Unit Test, when you're testing multiple actions

Question - Is using the static var node = SiteMaps.Current.CurrentNode; the best way to go in the controllers, or is there a better way of doing it (we use Unity)

Thanks for your help


Solution

  • Alternative

    For this specific use case, you don't need to access the static SiteMaps class at all. There is a SiteMapTitle action filter attribute in the MvcSiteMapProvider.Web.Mvc.Filters namespace that can be used to set the title based on your model.

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

    Mocking ISiteMapLoader

    As for setting the ISiteMapLoader, I now see that there is an issue because it is static. That means it will live throughout the lifecycle of the unit test framework's runner process regardless of how many tests are setup/torn down. Ideally, there would be a way to read the Loader property (or some other similar check) to see if it has already been populated and then skip that step if it is, but unfortunately that isn't the case.

    So, the next best thing would be to make a static helper class to track whether the ISiteMapLoader has been loaded, and skip the set operation if it is.

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

    Usage

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

    Of course, the downside is that your mock is not isolated to a specific unit test, so all of your mocking for your entire test suite must be done in a single place (assuming you need to mock other members of ISiteMapLoader and its dependencies).

    Another Possible Alternative

    If you are open to changing your testing framework, there is another possibility. You can setup your tests to each run in their own AppDomain, which should allow for the static ISiteMapLoader instance to be unloaded for each test.

    I discovered in this question that there is an NUnit.AppDomain package that can be used to do this.

    Someone also pointed out that XUnit automatically runs unit tests in separate AppDomains without additional configuration.

    If changing unit test frameworks is not an option, you might be able to get around this by putting each unit test that interacts with a static member into a separate assembly.

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

    Reference: MSTest & AppDomains