Isolating calls to Sitecore.Context for improved unit testability - Part II: ItemAdapter
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:
- Unit tests still require additional Sitecore assemblies and the Sitecore license file.
- Unit tests look a bit cluttered due to setting up the fake Db and DbItem.
- 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 theIItemAdapter
mock. - The
GetAuthorItemMock()
method contructs a mock object (authorItemMock
) based onIItemAdapter
and requires parameters for the Id, author name and company name. - The
GetItemProviderMock()
method constructs a mock (itemProviderMock
) based onIItemProvider
. TheauthorItemMock
is passed as a parameter since that will be the result of theGetItemAdapter()
method of the mock. - An instance is created of the
AuthorProvider
and theitemProviderMock
is passed in the constructor.
// Act
- The
GetAuthor()
method on theAuthorProvider
is called. Inside this method theGetAuthorItem()
method is called which in turn executes the set-upGetItemAdapter()
method of the mockedIItemProvider
. A mockedIItemAdapter
is returned and mapped to a newAuthor
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 mockedIItemAdapter
.
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.