Back

Isolating calls to Sitecore.Context for improved unit testability - Part II: ItemAdapter

I don't adapt to my environment, my enviroment adapts to me.

Recap of Part I

This is part two of the "Isolating calls to Sitecore.Context..." series. If you haven't read the Part I please do so to get the right context (pun intended).

In Part I the GetItem() method from ItemProvider returned an actual Sitecore Item. Because of the IItemProvider interface and Sitecore.FakeDb it is possible to return fake Sitecore items and no dependency to the Sitecore context is required in unit tests.

Although unit testing is now possible there are some (minor) downsides to them due to Sitecore.FakeDb:

  1. Unit tests still require additional Sitecore assemblies and the Sitecore license file.
  2. Unit tests look a bit cluttered due to setting up the fake Db and DbItem.
  3. Unit tests are not very fast to execute.

So lets look at another way of dealing with Sitecore items to get very lean unit tests.

Adapters

I prefer to use abstractions of Sitecore objects because they make unit testing so much easier. The abstractions act as an adapter. It wraps the Sitecore object and exposes some frequently used properties and methods of that object. The adapter or wrapper pattern in combination with Sitecore is quite common and has been described earlier by several others (e.g. Alistair Deneys and Martina Welander).

So instead of working directly with a Sitecore Item we can work with an IItemAdapter interface which is implemented by the ItemAdapter type.

IItemAdapter.cs

using System.Collections.Generic;
using Sitecore.Data;
using Sitecore.Data.Items;

namespace SitecorePlayground.Common.Interfaces.Adapters
{
    public interface IItemAdapter
    {
        string DisplayName { get; }

        ID Id { get; }

        Item InnerItem { get; }

        ID TemplateId { get; }

        string this[string fieldName] { get; }
    }
}

ItemAdapter.cs

using System.Collections.Generic;
using System.Linq;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using SitecorePlayground.Common.Interfaces.Adapters;

namespace SitecorePlayground.Common.Adapters
{
    public class ItemAdapter : IItemAdapter
    {
        public ItemAdapter(Item item)
        {
            Assert.ArgumentNotNull(item, "item");

            this.InnerItem = item;
        }

        public string DisplayName
        {
            get { return InnerItem.DisplayName; }
        }

        public ID Id
        {
            get { return InnerItem.ID; }
        }

        public Item InnerItem
        {
            get;
            private set;
        }

        public ID TemplateId
        {
            get { return InnerItem.TemplateID; }
        }

        public string this[string fieldName]
        {
            get { return InnerItem.Fields[fieldName].Value; }
        }
    }
}

Note that the original Sitecore Item is accessible through the InnerItem property.

Code that should be unit testable should rely only on the other properties the adapter exposes. Code that requires Item properties which are not exposed directly by the ItemAdapter (and don't require unit testing) could use the InnerItem property.

Let's have a look now at the new IItemProvider interface and ItemProvider implementation.

IItemProvider.cs

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

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

        IItemAdapter GetItemAdapter(ID itemId);
    }
}

ItemProvider.ss

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

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

        public IItemAdapter GetItemAdapter(ID itemId)
        {
            var item = GetItem(itemId);
            return item != null ? new ItemAdapter(item) : null;
        }
    }
}

A new method is added called GetItemAdapter(). When in the web context the ItemProvider will call it's own GetItem() method which will return an actual Sitecore Item and wrap it in an ItemAdapter. In a unit test context however IItemProvider will be mocked and the GetItemAdapter() method will be set-up to return a fake ItemAdapter (i.e. not based on a Sitecore Item).

Let's recall the AuthorProvider example which was used in part I. Here's the new AuthorProvider class where the GetAuthorItem() method now calls the GetItemAdapter() method of the ItemProvider and thus returning an IItemAdapter.

AuthorProviderBasedOnItemAdapter.cs

using Sitecore.Data;
using SitecorePlayground.Common.Interfaces.Adapters;
using SitecorePlayground.Common.Interfaces.Providers;
using SitecorePlayground.News.Models;

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

        public AuthorProviderBasedOnItemAdapter(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 IItemAdapter GetAuthorItem(ID authorItemId)
        {
            return itemProvider.GetItemAdapter(authorItemId);
        }
    }
}

Unit tests with IItemAdapter and Moq

Here is the unit test for the GetAuthor() method when the AuthorProvider works with an IItemAdapter.

AuthorProviderBasedOnItemAdapterTests.cs

using System;
using Moq;
using NUnit.Framework;
using Sitecore.Data;
using SitecorePlayground.Common.Interfaces.Adapters;
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 AuthorProviderBasedOnItemAdapter.
    /// </summary>
    [TestFixture]
    public class AuthorProviderBasedOnItemAdapterTests
    {
        [Test]
        public void GetAuthor_WithValidAuthorBasedOnItemAdapter_ReturnsAuthorObject()
        {
            // Arrange
            var authorItemId = new ID(Guid.NewGuid());
            var authorItemMock = GetAuthorItemMock(authorItemId, "John West", "Sitecore");
            var itemProviderMock = GetItemProviderMock(authorItemMock.Object);
            var authorProvider = new AuthorProviderBasedOnItemAdapter(itemProviderMock.Object);

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

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

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

            return itemProviderMock;
        }

        private static Mock<IItemAdapter> GetAuthorItemMock(ID itemId, string authorName, string companyName)
        {
            var itemMock = new Mock<IItemAdapter>();
            itemMock.SetupGet(mock => mock.TemplateId).Returns(new ID(AuthorTemplate.TemplateId));
            itemMock.SetupGet(mock => mock.Id).Returns(itemId);
            itemMock.SetupGet(mock => mock[AuthorTemplate.Fields.AuthorName]).Returns(authorName);
            itemMock.SetupGet(mock => mock[AuthorTemplate.Fields.AuthorCompany]).Returns(companyName);

            return itemMock;
        }
    }
}

When compared with the unit test in the first post (which used FakeDb) this unit test is slightly more compact and easier to understand. Don't get me wrong, I really like Sitecore.FakeDb but use it only when you can't use an adapter.

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

// Arrange

  • First a new Id is generated which will be used for the IItemAdapter mock.
  • The GetAuthorItemMock() method contructs a mock object (authorItemMock) based on IItemAdapter and requires parameters for the Id, author name and company name.
  • The GetItemProviderMock() method constructs a mock (itemProviderMock) based on IItemProvider. The authorItemMock is passed as a parameter since that will be the result of the GetItemAdapter() method of the mock.
  • An instance is created of the AuthorProvider 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 GetItemAdapter() method of the mocked IItemProvider. A mocked IItemAdapter 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 mocked IItemAdapter.

Conclusion

Creating adapters for Sitecore objects can be a relatively quick way to get unit testable code as long as dependency injection principles are used. You are in complete control of the adapter interface. You can start with a very lightweight interface and just expose a couple of properties you need for proper unit testing. Then you can gradually introduce additional properties to the interface as needed.

Next to the Sitecore Item, other frequently adapted Sitecore objects are Database, Context and SiteContext. More of that in a later post.

Source code

The full source code that belongs to this post (and more) can be found on Github.