Unit Testing in Sitecore

Unit Testing

I have been playing around with Unit Testing in Sitecore today. In conducting my research, I came across a very useful series of blog posts that cover this in detail (https://www.codeflood.net/blog/2020/05/17/logicless-view-itemless-model/)


A summary of the principles covered are below:

Keep business logic out of the views

Business logic buried inside a view can only be used inside that view. This is means the logic cannot be reused and generally makes unit testing quite difficult. To allow the business logic to be reused and properly unit tested, the logic must be kept in some other place outside of the view. Keep the view purely for presentation to transform from your model to the markup required for your channel (HTML).

Keep Item out of the model

With the APIs Sitecore offers and the prevelant use of the Item class, it may feel natural to use an item instance as the model in your project. Patterns such as the Custom Item Pattern also contribute to this confusion as custom items are a kind of model. The fact that the Item class is now easily mockable means unit testing is no longer a concern for this principal. But in production code, items cannot be instantiated directly. They must always be loaded from a database. Dependeing on how you use the item instances, this can cause performance issues at scale. It also means your model can never be provided from any other location other than a Sitecore database.

Encapsulate Logic

This principal is closely related to the “Keep business logic out of the views” principal above. To ensure logic can be reused, it must be encapsulated so it can be shared between components. Encapsulating logic also makes it easier to test the logic in isolation of any concerns of where it is used.

Use Dependency Injection and Abstractions

This principal possibly has the biggest impact on how complicated your unit tests will end up being. If a components dependencies cannot be specified at test-time, then those dependencies cannot be controlled and mocked. This principal ties back to the good old Object Oriented principal of “code to an abstraction” which specifies that classes should not contain references to concrete implementations, but only to abstractions. This makes changing the dependency much easier, whether that be a new dependency in production code or switching a real implementation to a test mock for testing purposes.

Avoid Implicit Data

Related to the above principal, make a classes dependencies explicit and clear. That will make the class much more reusable as the logic of the class is no longer tied to the implicit data and whatever it holds. Implicit data can also complicate tests, especially if that implicit data cannot be directly set.

Use Sitecore Abstractions

Sitecore contains a large number of static classes which provide access to various servies. These services are all now registered with the service provider as an appropriate abstraction. This allows code to follow the “Use Dependency Injection and Abstractions” principal and avoid making implicit service calls, which may fail in a test context. By using the appropriate Sitecore abstractions for things like manager classes, it makes it much easier to mock the class and inject it for tests.


Creating my own tests

Using the above, I then put together a set of tests to unit test an ImageTaxonomy class from one of my sitecore projects.

This uses xUnit as the testing framework and nSubstitute to mock the dependencies:

[TestClass]
public class ImageTaxonomyTests
{
    private string mediaItemIDString = "12345678-1234-1234-1234-123456789012";
    private ID mediaItemID = new ID("12345678-1234-1234-1234-123456789012");

    [Fact]
    public void Ctor_MediaManagerIsNull_Throws()
    {
        // arrange
        Action sutAction = () => new ImageTaxonomy(null);

        // act, assert
        var ex = Xunit.Assert.Throws<ArgumentNullException>(sutAction);
        Xunit.Assert.Equal("mediaManager", ex.ParamName);
    }

    [Fact]
    public void GetImageUrl_ItemIsNull_ReturnsEmpty()
    {
        // arrange
        var mediaManager = Substitute.For<BaseMediaManager>();
        var imageUrl = "~/Test-Image";
        mediaManager.GetMediaUrl(Arg.Any<MediaItem>()).Returns(imageUrl);
        var sut = new ImageTaxonomy(mediaManager);

        // act
        var results = sut.GetImageUrl(null, "Some Field");

        // assert
        Xunit.Assert.Empty(results);
    }

    [Fact]
    public void GetImageUrl_FieldNameIsNull_ReturnsEmpty()
    {
        // arrange
        var database = Substitute.For<Database>();
        var mediaManager = Substitute.For<BaseMediaManager>();

        var item = CreateItem(database);

        var sut = new ImageTaxonomy(mediaManager);

        // act
        var results = sut.GetImageUrl(item, null);

        // assert
        Xunit.Assert.Empty(results);
    }

    [Fact]
    public void GetImageUrl_UnknownFieldName_ReturnsEmpty()
    {
        // arrange
        var database = Substitute.For<Database>();
        var mediaManager = Substitute.For<BaseMediaManager>();

        var item = CreateItem(database);

        var sut = new ImageTaxonomy(mediaManager);

        // act
        var results = sut.GetImageUrl(item, "Some Unknown Field");

        // assert
        Xunit.Assert.Empty(results);
    }

    [Fact]
    public void GetImageUrl_KnownFieldName_ReturnsValue()
    {
        // arrange
        var database = Substitute.For<Database>();

        var item = CreateItem(database);
        SetItemField(item, "Some Known Field", $"<image mediaid='{mediaItemIDString}' />");

        var mediaItem = CreateMediaItem(database);
        database.GetItem(mediaItemID, Arg.Any<Language>(), Sitecore.Data.Version.Latest).Returns(mediaItem);

        var mediaManager = Substitute.For<BaseMediaManager>();
        mediaManager.GetMediaUrl(Arg.Is<MediaItem>(mi => mi.ID == mediaItem.ID)).Returns("/a/path/to/an/image.jpg");

        var sut = new ImageTaxonomy(mediaManager);

        // act
        var results = sut.GetImageUrl(item, "Some Known Field");

        // assert
        Xunit.Assert.Equal("/a/path/to/an/image.jpg", results);
    }

    private Item CreateItem(Database database = null)
    {
        var item = Substitute.For<Item>(ID.NewID, ItemData.Empty, database);
        var fields = Substitute.For<FieldCollection>(item);
        item.Fields.Returns(fields);
        return item;
    }
    private Item CreateMediaItem(Database database = null)
    {
        var definition = new ItemDefinition(mediaItemID, "Mock Media Item", ID.Null, ID.Null);
        var data = new ItemData(definition, Language.Current, Sitecore.Data.Version.First, new FieldList());
        var mediaItem = new Item(mediaItemID, data, database);
        return mediaItem;
    }
    private void SetItemField(Item item, string fieldName, string fieldValue)
    {
        var field = Substitute.For<Field>(ID.NewID, item);
        field.Value = fieldValue;
        field.Database.Returns(item.Database);
        item.Fields[fieldName].Returns(field);
    }
}

I initially struggled with a couple of points:

  1. How to mock an ImageField
    Credit to Serhii Shushliapin, who helped me resolve the issue here:
    https://sitecore.stackexchange.com/questions/30692/how-to-mock-an-imagefield-in-sitecore-using-nsubstitute-and-xunit
  2. How to mock Media Manager
    Credit to Marek Musielak, who helped resolve the issue here:
    https://sitecore.stackexchange.com/questions/30703/how-to-mock-mediamanager-in-sitecore-using-nsubstitute-and-xunit

Leave a Reply

Your email address will not be published. Required fields are marked *