Highly testable Sitecore code using MVC, Ninject, Synthesis, Moq, and NUnit

Posted 09/15/2014 by Benjamin Chaikin

Download the full source and Sitecore package here

The problem

The Sitecore API is great at giving us a powerful way to access our content. This power and flexibility allows us to build big, industrial strength sites – including large multi-tenant, multi-language deployments with lots of complex logic. This level of complexity begs for automated unit testing. So what’s the problem?

  1. The bulk of the public Sitecore API is exposed as concrete classes and static methods, rather than sitting behind interfaces. These calls eventually spread tightly-coupled code to the far corners of your codebase. The end result is we wind up writing integration tests that create SC items during setup and wipe them during tear down. Integration testing is great, but it serves a different purpose than unit testing.
  2. Sitecore objects expect to live within the Sitecore runtime. Without a valid context they shrivel and die, forcing developers into clever yet awkward gymnastics to run tests. Things get worse as part of a continuous build.

A solution

We want to get to a place where we can write isolated unit tests with familiar tools. In order to do this we’re going to leverage the following components:

  1. Sitecore MVC. Really all of this can be done in webforms, but it involves serious design discipline.
  2. An IoC container to facilitate dependency injection. I like Ninject.
  3. Some glue code to wire your IoC container into the Sitecore MVC pipeline. I’ll be using this awesome code by Thomas Stern.
  4. Your favorite Sitecore object mapper. Mine is Synthesis at the moment, but Glass seems pretty nice as well.
  5. A wrapper class or two that will act as a barrier between your code and Sitecore’s. I started with this set of wrappers courtesy of Marty Woods and added a few things as needed.

The actual testing can be done with whatever tools you’re already comfortable with. For this demo I’ll be using Moq, NUnit, and the NUnit Test Adapter for Visual Studio. In addition I am using the Synthesis.Testing package to make mocking Synthesis fields a snap. All of the components necessary (excluding the two blogs) can be easily pulled into your project(s) via NuGet package manager command line or via Visual Studio extensions.

Architecture

Here’s what our stack will look like once everything is in place:

All of our components (including the Sitecore API) will sit behind an interface, allowing Ninject to handle their creation automatically and our unit tests to run in isolation. Our code will be talking to Sitecore via ISitecoreContext and Synthesis, never directly.

Wrapping the Sitecore Context

The interface ISitecoreContext along with some help from Synthesis will seal off our code from Sitecore. Here we expose commonly needed properties – things like the database, the current item, data source, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Sitecore.Mvc.Presentation;
using Synthesis;
using Synthesis.FieldTypes.Adapters;
 
namespace MvcTestingDemo.Business.Adapters
{
    public interface ISitecoreContext
    {
        IStandardTemplateItem CurrentItem { get; }
        IStandardTemplateItem HomepageItem { get; }
        bool IsPageEditor { get; }
        bool IsPreview { get; }
        ISiteContextAdapter Site { get; }
        IDatabaseAdapter Database { get; }
        RenderingParameters Parameters { get; }
        IStandardTemplateItem DatasourceItem { get; }
        string Placeholder { get; }
    }
}

Wait, but feature XYZ is missing! You’re right, but the pattern holds. Add an interface method and write the implementation as you need.

Plugging Ninject into Sitecore

We will be tapping into Sitecore MVC’s controller pipeline by creating our own factory that will be responsible for spinning up controllers using Ninject.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Ninject;
using System;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcTestingDemo.SCExtensions.Ninject
{
    /// <summary>
    /// </summary>
    public class NinjectControllerFactory : DefaultControllerFactory
    {
        private IKernel _kernel;
 
        public NinjectControllerFactory(IKernel kernel)
        {
            _kernel = kernel;
        }
        public override void ReleaseController(IController controller)
        {
            _kernel.Release(controller);
        }
 
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return (IController)_kernel.Get(controllerType);
        }
    }
}

The following is plugged into Sitecore via a simple config patch:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8" ?><configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
               xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc"
                   set:type="MvcTestingDemo.SCExtensions.Ninject.InitializeNinjectControllerFactory, MvcTestingDemo.SCExtensions" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

The NinjectKernelFactory executes once when the app pool spins up. Is uses reflection to seek out and instantiate any Ninject modules that happen to be in the current AppDomain. By default we load our Sitecore context wrapper. In the web project we add our business logic mappings specific to the site. Any number of modules can be added and will load automatically on start up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using MvcTestingDemo.Business.Adapters;
using Ninject.Modules;
 
namespace MvcTestingDemo.SCExtensions.Ninject.Modules
{
    public class FrameworkModule : NinjectModule
    {
        public override void Load()
        {
            Bind<ISitecoreContext>().To<SitecoreContext>();
        }
    }
}
 
// *******************************
 
using MvcTestingDemo.Business;
using Ninject.Modules;
 
namespace MvcTestingDemo.App_Start
{
    public class MvcDemoModule : NinjectModule
    {
        public override void Load()
        {
            // Custom dependency mappings go here
            Bind<IMenuLogic>().To<MenuLogic>();
        }
    }
}

At this point we’re good to use constructor injection in all of our controllers, and in all of our business components that our controllers might need!

MVC Demo Site

The demo site I have put together is a simple Bootstrap template with a few placeholders added for content, and a dynamically generated menu that is driven by the content hierarchy. The menu component will serve as an example of how we can pull content dynamically from Sitecore in a testable way.

And the underlying content tree:

We are using a Razor for both the layout and sublayouts. Controller renderings drive the sublayout components in order to take advantage of the dependency injection. Here is our menu controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using MvcTestingDemo.Business;
using System.Web.Mvc;
 
namespace MvcTestingDemo.Controllers
{
    public class MenuController : Controller
    {
        private readonly IMenuLogic _menuLogic;
 
        // Using constructor injection to keep components isolated
        public MenuController(IMenuLogic menuLogic)
        {
            _menuLogic = menuLogic;
        }
 
        [HttpGet]
        public ActionResult Index()
        {
            var model = _menuLogic.GetMenuItems();
            return View("~/Views/Renderings/Navigation/Menu.cshtml", model);
        }
    }
}

There’s not much to see, but we’re using constructor injection to get our business logic class which will do the heavy lifting. I’m a big fan of keeping controller actions as clean as possible – they should be responsible for calling the correct business components, and maybe messing with the Http response, but not much else. 

Also note that I've chosen to reference the view explicitly, rather than using the Asp.net MVC convention-based approach of /Views/[controller]/[action].cshtml. This gives us the ability to maintain an an organized view folder structure that more closely matches the Sitecore content tree. If you prefer to use the convention-based approach, Sitecore MVC supports that as well.

Here’s the actual business logic to drive our dynamic menu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using MvcTestingDemo.Business.Adapters;
using MvcTestingDemo.Models;
using System.Collections.Generic;
using System.Linq;
 
namespace MvcTestingDemo.Business
{
    public class MenuLogic : IMenuLogic
    {
        private readonly ISitecoreContext _context;
 
        public MenuLogic(ISitecoreContext context)
        {
            _context = context;
        }
 
        public IEnumerable<MenuItemModel> GetMenuItems()
        {
            // Using the context wrapper allows us to isolate ourselves from the Sitecore API
            // Not generally best practice to pull an item by path, but for the purposes of our demo...
            var menuContainer = _context.Database.GetItem("/sitecore/content/home");      
            var home = menuContainer as INavigableItem;
 
            if (home != null)
            {
                return GetMenuItem(home).Children;
            }
 
            return Enumerable.Empty<MenuItemModel>();
        }
         
        private MenuItemModel GetMenuItem(INavigableItem item)
        {
            var result = new MenuItemModel()
            {
                MenuItem = item,
            };
 
            result.Children = item.Axes.GetChildren()
                .OfType<INavigableItem>()
                .Where(i => i.IncludedInNavigation.Value)
                .Select(c => GetMenuItem(c));
 
            return result;
        }   
    }
}

Here we are getting an injected Sitecore context and using it to grab the home item and recursively walk its children. The interface INavigableItem was generated by Synthesis and maps to the Navigable template from which our content pages are derived. We include only pages marked as IncludedInNavigation, and return the result as a custom type MenuItemModel. It’s not rocket science, but it gives us a few paths we can test!

The Unit Tests

Now free of external dependencies, our business logic can be tested. We will use Moq along with the Synthesis.Testing module in order to setup hypothetical scenarios, execute our business logic, and assert that it behaved in the way we expected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
using Moq;
using MvcTestingDemo.Business;
using MvcTestingDemo.Business.Adapters;
using MvcTestingDemo.Models;
using NUnit.Framework;
using Synthesis.FieldTypes.Adapters;
using Synthesis.Testing;
using System.Collections.Generic;
using System.Linq;
 
namespace MvcTestingDemo.UnitTests
{
    [TestFixture]
    public class MenuLogicTests
    {
        private MenuLogic _testSubject;
        private Mock<ISitecoreContext> _context;
        private Mock<IDatabaseAdapter> _database;
 
        [SetUp]
        public void SetUp()
        {
            _context = new Mock<ISitecoreContext>();
            _database = new Mock<IDatabaseAdapter>();
            _context.Setup(c => c.Database).Returns(_database.Object);
 
            _testSubject = new MenuLogic(_context.Object);
        }
 
        [Test]
        public void GetItems_HomeMissing_ReturnsEmptyCollection()
        {
            // Arrange
            INavigableItem home = null;
            _database.Setup(db => db.GetItem(It.IsAny<string>())).Returns(home);
 
            // Act
            var result = _testSubject.GetMenuItems();
 
            // Assert
            Assert.IsNotNull(result);
            Assert.IsEmpty(result);
        }
 
        [Test]
        public void GetItems_OneChild_ReturnsOne()
        {
            // Arrange
            var home = new Mock<INavigableItem>();
            var axesAdapter = new Mock<IAxesAdapter>();
            var item1 = new Mock<INavigableItem>();
            item1.Setup(i => i.IncludedInNavigation).Returns(new TestBooleanField(true));
            item1.Setup(i => i.Axes).Returns(new Mock<IAxesAdapter>().Object);
            var children = new List<INavigableItem>()
            {
                item1.Object,
            };
 
            home.SetupGet(h => h.Axes).Returns(axesAdapter.Object);
            axesAdapter.Setup(a => a.GetChildren()).Returns(children);
 
            _database.Setup(db => db.GetItem("/sitecore/Content/Home")).Returns(home.Object);
 
            // Act
            var result = _testSubject.GetMenuItems();
 
            // Assert
            Assert.IsNotNull(result);
            Assert.AreEqual(1, result.Count());
        }
 
        [Test]
        public void GetItems_OneExcludedOneNot_ReturnsOne()
        {
            // Arrange
            var home = new Mock<INavigableItem>();
            var axesAdapter = new Mock<IAxesAdapter>();
 
            var item1 = new Mock<INavigableItem>();
            item1.Setup(i => i.IncludedInNavigation).Returns(new TestBooleanField(true));
            item1.Setup(i => i.Axes).Returns(new Mock<IAxesAdapter>().Object);
 
            var item2 = new Mock<INavigableItem>();
            item2.Setup(i => i.IncludedInNavigation).Returns(new TestBooleanField(false));
            item2.Setup(i => i.Axes).Returns(new Mock<IAxesAdapter>().Object);
 
            var children = new List<INavigableItem>()
            {
                item1.Object,
                item2.Object
            };
 
            home.SetupGet(h => h.Axes).Returns(axesAdapter.Object);
            axesAdapter.Setup(a => a.GetChildren()).Returns(children);
 
            _database.Setup(db => db.GetItem("/sitecore/Content/Home")).Returns(home.Object);
 
            // Act
            var result = _testSubject.GetMenuItems();
 
            // Assert
            Assert.IsNotNull(result);
            Assert.AreEqual(1, result.Count());
        }
    }
}

Conclusion

The extreme isolation we've put together above may seem like overkill for a lot of situations. If you have a dead-simple widget that does nothing more than spit a field or two onto the page then you might be better off with a simple view rendering and no controller. The benefits of isolation really start to show when working with complex components which contain real business logic. They are even more apparent when building large systems with teams of developers. Make the pragmatic choice, but err on the side of testability!

Download the full source and Sitecore package here

Share:

Archive

Syndication