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