Back

Isolating calls to Sitecore.Context for improved unit testability - Part I: ItemProvider, Moq and FakeDb

I find your lack of unit tests disturbing!

Sitecore projects and (un)testable code

Over the last years I've been involved with quite some Sitecore projects, some were true greenfield projects where a solution is created from scratch and some involved 'only' customizing components or extending the existing platform with new functionality. I enjoy both types of projects since they each have their challenges. I do want to share my concern from what I've seen in some of the latter solutions. Some things that all of these projects had in common were:

  1. Little to no utilization of an ORM, such as Glass Synthesis, CDM (or a well defined self made solution).
  2. Lack of proper testable code (no dependency injection).
  3. Lack of unit tests

Of course all these three points are related. If maintainability is important it is vital to any software project that code is written in such a way that it is unit testable. Although this post concerns isolating Sitecore, it could as well be about isolating calls to a custom database or to a logging component.

This post is intended as a practical guide for the ones involved with these 'difficult' projects and are strongly in favor of improving the code base in order to improve the testability and maintainability without spending many man months up front to make it happen.

Isolating calls to the Sitecore context

The biggest problem I noticed with some Sitecore solutions is that calls to Sitecore.Context.Database.GetItem() are all over the place.

The first thing that can be done is to isolate these calls and put this in a custom ItemProvider class. (Note that Sitecore has its own ItemProvider class in the Sitecore.Kernel.dll but we're not touching that one.) So let's start with the following very basic interface (IItemProvider) and implementation (ItemProvider). It will get more interesting later, I promise.

IItemProvider.cs

using Sitecore.Data;
using Sitecore.Data.Items;

namespace SitecorePlayground.Common.Interfaces.Providers
{
    public interface IItemProvider
    {
        Item GetItem(ID itemId);
    }
}

ItemProvider.cs

using Sitecore.Data;
using Sitecore.Data.Items;
using SitecorePlayground.Common.Interfaces.Providers;

namespace SitecorePlayground.Common.Providers
{
    public class ItemProvider : IItemProvider
    {
        public Item GetItem(ID itemId)
        {
            return Sitecore.Context.Database.GetItem(itemId);
        }
    }
}

In every Sitecore solution C# models are used which are based on Sitecore templates. Let's assume we are dealing with the following Author object in C#.

Author.cs

namespace SitecorePlayground.News.Models
{
    public class Author
    {
        public string Name { get; set; }

        public string Company { get; set; }
    }
}

Author instances are usually retrieved via a specific provider such as the AuthorProvider below. (The class name in the gist below is a bit longer because I'll show another flavor of this provider in a next post).

AuthorProviderBasedOnRegularItem.cs

using Sitecore.Data;
using Sitecore.Data.Items;

using SitecorePlayground.Common.Interfaces.Providers;
using SitecorePlayground.News.Models;

namespace SitecorePlayground.News.Providers
{
    public class AuthorProviderBasedOnRegularItem
    {
        private readonly IItemProvider itemProvider;

        public AuthorProviderBasedOnRegularItem(IItemProvider itemProvider)
        {
            this.itemProvider = itemProvider;
        }

        public Author GetAuthor(string authorId)
        {
            ID parsedAuthorId;
            if (!ID.TryParse(authorId, out parsedAuthorId))
            {
                return null;
            }

            return this.GetAuthor(parsedAuthorId);
        }

        public Author GetAuthor(ID authorId)
        {
            var authorItem = GetAuthorItem(authorId);

            if (authorItem == null)
            {
                return null;
            }

            return new Author
            {
                Company = authorItem[Templates.AuthorTemplate.Fields.AuthorCompany],
                Name = authorItem[Templates.AuthorTemplate.Fields.AuthorName]
            };
        }

        private Item GetAuthorItem(ID authorItemId)
        {
            return itemProvider.GetItem(authorItemId);
        }
    }
}

Notice that the constructor of AuthorProvider requires an instance of a type that implements IItemProvider (this is an example of constructor injection). The GetAuthorItem() method calls the GetItem() method on the IItemProvider and this construction enables us to unit test the AuthorProvider using Sitecore.FakeDb and Moq (or any other mocking framework you prefer).

Unit tests with Sitecore.FakeDb and Moq

Sitecore.FakeDb is a very nice unit testing framework which allows creation and manipulation of Sitecore items in memory.It's quite easy to get started with. Just install the NuGet package and follow the instructions carefully because some Sitecore & Lucene assemblies and a valid Sitecore license are required. In the example below I only use FakeDb as a source for getting Sitecore items. Moq is a very popular mocking framework. If you don't know it make sure you read at least the [quickstart[(https://github.com/Moq/moq4/wiki/Quickstart).

Here is the unit test for the AuthorProvider.GetAuthor() method.

AuthorProviderBasedOnRegularItem.cs

using System;
using Moq;
using NUnit.Framework;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.FakeDb;
using SitecorePlayground.Common.Interfaces.Providers;
using SitecorePlayground.News.Models;
using SitecorePlayground.News.Providers;
using SitecorePlayground.News.Templates;

namespace SitecorePlayground.News.Test.Providers
{
    /// <summary>
    /// Unit tests for the AuthorProviderBasedOnRegularItem class.
    /// </summary>
    [TestFixture]
    [Category("Requires Sitecore.FakeDb and Sitecore license")]
    public class AuthorProviderBasedOnRegularItemTests
    {
        [Test]
        public void GetAuthor_WithValidAuthorBasedOnRegularItem_ReturnsAuthorObject()
        {
            using (var fakeDb = new Db())
            {
                // Arrange
                var authorId = new ID(Guid.NewGuid());
                var templateId = new ID(AuthorTemplate.TemplateId);
                DbItem fakeDbItem = GetFakeAuthorDbItem("John West", "Sitecore", authorId, templateId);
                fakeDb.Add(fakeDbItem);
                var fakeAuthorItem = fakeDb.GetItem(authorId);
                var itemProviderMock = GetItemProviderMock(fakeAuthorItem);
                var authorProvider = new AuthorProviderBasedOnRegularItem(itemProviderMock.Object);

                // Act
                Author result = authorProvider.GetAuthor(authorId);

                // Assert
                Assert.AreEqual("John West", result.Name);
            }
        }

        private DbItem GetFakeAuthorDbItem(string authorName, string authorCompany, ID itemId, ID templateId)
        {
            return new DbItem(authorName, itemId, templateId)
                       {
                           { AuthorTemplate.Fields.AuthorName, authorName }, 
                           { AuthorTemplate.Fields.AuthorCompany, authorCompany }
                       };
        }

        private Mock<IItemProvider> GetItemProviderMock(Item authorItem)
        {
            var itemProviderMock = new Mock<IItemProvider>();
            itemProviderMock.Setup(mock => mock.GetItem(It.IsAny<ID>())).Returns(authorItem);

            return itemProviderMock;
        }
    }
}

The goal of the unit test is to verify if the GetAuthor() method of the AuthorProvider returns an Author object when a Sitecore item Id is passed in as a parameter. When the AuthorProvider is used in a website context an ItemProvider is passed into the constructor of the AuthorProvider and the item is retrieved from the Sitecore context. In the unit test however we don't want any dependency on the Sitecore context. Therefore an mock is created based on the IItemProvider interface. We can set-up the GetItem() method on the mock to return a fake Sitecore item which we will get from Sitecore.FakeDb.

Let's look at the the unit test in more detail.

// Arrange

  • Since FakeDb is an in memory database an instance is created inside a using statement. This ensures that the in memory database is disposed properly after running the unit test.
  • In order to create a new DbItem (from Sitecore.FakeDb) an item Name, Id and Template Id are required. The GetFakeAuthorDbItem method constructs the DbItem with fields for the author name and the company.
  • Once the DbItem is created and added to the FakeDb instance we retrieve the Sitecore item (fakeAuthorItem) from FakeDb.
  • Next an itemProviderMock object is created based on the IItemProvider and the fakeAuthorItem is passed since that is used as the returning item for the mocked GetItem() method (see the GetItemProviderMock method how that is set-up).
  • In the final line of the Arrange section an instance of the AuthorProvider is created and the itemProviderMock is passed in the constructor.

// Act

  • The GetAuthor() method on the AuthorProvider is called. Inside this method the GetAuthorItem() method is called which in turn executes the set-up GetItem() method of the mocked IItemProvider. A Sitecore item (from FakeDb) is returned and mapped to a new Author object.

// Assert

  • An assertion is done to check if the Name property of the Author object is equal to the author name field of the Sitecore item.

Conclusion

Though this example is fairly straightforward it demonstrates how to write testable code when you're dealing with Sitecore projects. Writing testable code and using a mocking framework in combination with Sitecore.FakeDb in unit tests can be a bit of a learning curve but I consider these as must have skills for any Sitecore developer these days.

In the next post I'll show a similar approach with an ItemProvider that uses an ItemAdapter instead of a regular Sitecore item.

Source code

The full source code that is used in this post (and lots more) is on GitHub. Feel free to poke at it and suggest improvements.