codeflood logo

Unit Testing Sitecore Components Part 2: Encapsulate Logic

In the previous post of this series I detailed 2 principals which can help with making Sitecore components more testable and reusable. These were "keeping the business logic out of the view" and "keeping the Item class out of the model". In this post I'll detail several more principals to continue improving the code.

This post is part of a series covering the principals I showed during my virtual SUGCON presentation on unit testing Sitecore components in April of this year (2020). The following is a list of the posts belonging to the series so far, along with the principals covered in the post:

Unit Testing Sitecore Components Part 1: Logicless Views and Itemless models

  • Principal: Keep business logic out of the view.
  • Principal: Keep Item out of the model.

Unit Testing Sitecore Components Part 3: Avoid Static Members

  • Principal: Avoid implicit data
  • Principal: Avoid statics

Unit Testing Sitecore Components Part 4: Mocking Items and Fields

  • How to mock the Sitecore.Data.Items.Item class.
  • How to mock field values of a mocked item.

Unit Testing Sitecore Components Part 5: Recap and Resources

  • Recap
  • Resources

In this post, I'll cover 2 more principals:

  • Principal: Encapsulate logic.
  • Principal: Use Dependency Injection and Abstractions

Example Rendering

I'll continue to use the same view rendering which I started refactoring in the previous post. Here's what I ended up with at the end:

@model UnitTestingSitecoreComponents.Web.RenderingModels.EntryCategoriesRenderingModel

@if(Model.Categories.Any())
{
    <div class="wb-entry-categories wb-panel">
        <h3>(View Rendering) Posted in:</h3>
        <ul>
            @foreach (var category in @Model.Categories)
            {
                <li>
                    <a href="@category.Url">
                        @category.Title
                    </a>
                </li>
            }
        </ul>
    </div>
}

And the rendering model:

public class EntryCategoriesRenderingModel : RenderingModel
{
    public IEnumerable<Category> Categories { get; set; }

    public override void Initialize(Rendering rendering)
    {
        base.Initialize(rendering);

        var categoryField = (MultilistField)Item.Fields["Category"];
        var items = categoryField?.GetItems() ?? Enumerable.Empty<Item>();
        Categories = from item in items
                            select new Category
                            {
                                Title = item["Title"],
                                Url = LinkManager.GetItemUrl(item)
                            };
    }
}

And the POCO category model exposed on the Categories property:

namespace UnitTestingSitecoreComponents.Web.Models
{
    public class Category
    {
        public string Title { get; set; }

        public string Url { get; set; }
    }
}

The view and the Category class are nice and clean and I don't need to refactor them any further. Additional refactorings will focus on the EntryCategoriesRenderingModel class.

Encapsulate Logic

When applying the "Keep business logic out of the views" principal to the example rendering I moved any business logic from the view into the model, to get it out of the view. This is covered in the previous post. However the logic shouldn't remain in the model. I can easily test a rendering model; I can instantiate it in test code, call the Initialize method and validate the values of the properties which have been populated. Although the rendering model is testable the business logic it contains cannot be reused in other code. The logic could only be used with other views, and only other views that didn't require any additional data. What if I needed to generate the list of categories for an entry during some data export operation?

To make the business logic reusable, I want to encapsulate it into a separate class. That class can then be used by any code.

I'll go ahead and encapsulate the logic into a class named EntryTaxonomy.

public class EntryTaxonomy
{
    public IEnumerable<Category> GetCategories(Item item)
    {
        var categoryField = (MultilistField)item.Fields["Category"];
        var items = categoryField?.GetItems() ?? Enumerable.Empty<Item>();
        var categories = from item in items
                            select new Category
                            {
                                Title = item["Title"],
                                Url = LinkManager.GetItemUrl(item)
                            };

        return categories;
    }
}

public class EntryCategoriesRenderingModel : RenderingModel
{
    public IEnumerable<Category> Categories { get; set; }

    public override void Initialize(Rendering rendering)
    {
        base.Initialize(rendering);

        var taxonomy = new EntryTaxonomy();
        Categories = taxonomy.GetCategories(Item);
    }
}

If I were to jump in and start writing tests for EntryCategoriesRenderingModel now, they would be more complicated than they need to be. Although I've encapsulated the business logic into a separate class which can be tested separately, I would still need to go through mocking all the Sitecore data which EntryTaxonomy requires.

Use Dependency Injection and Abstractions

With EntryCategoriesRenderingModel in it's current state, it can only ever work with the EntryTaxonomy implementation above. This is because EntryCategoriesRenderingModel creates it's own instance of EntryTaxonomy to use. In a testing context, we want to be able to control the instances which the code under test is collaborating with so we can mock their behaviour, rather than having to use real instances and behaviour. This is done using dependency injection. Dependency injection is a practice in which we pass any dependencies to the class which needs them. In this case, I'll pass the EntryTaxonomy class instance into the constructor of EntryCategoriesRenderingModel.

public class EntryCategoriesRenderingModel : RenderingModel
{
    private EntryTaxonomy _entryTaxonomy = null;

    public IEnumerable<Category> Categories { get; set; }

    public EntryCategoriesRenderingModel(EntryTaxonomy entryTaxonomy)
    {
        _entryTaxonomy = entryTaxonomy;
    }

    public override void Initialize(Rendering rendering)
    {
        base.Initialize(rendering);

        Categories = _entryTaxonomy.GetCategories(Item);
    }
}

To be able to mock EntryTaxonomy for my tests, the class must have the appropriate virtual members, and at the moment it has no virtual members. Instead of passing the concrete EntryTaxonomy class to EntryCategoriesRenderingModel, I'll abstract the relevant methods to an interface and pass that in instead. This allows me to pass any implementation of the interface into EntryCategoriesRenderingModel whether it be the real EntryTaxonomy implementation or a mocked implementation.

public interface IEntryTaxonomy
{
    IEnumerable<Category> GetCategories(Item item);
}
public class EntryTaxonomy : IEntryTaxonomy
{
    public IEnumerable<Category> GetCategories(Item item)
    {
        var categoryField = (MultilistField)item.Fields["Category"];
        var items = categoryField?.GetItems() ?? Enumerable.Empty<Item>();
        var categories = from item in items
                            select new Category
                            {
                                Title = item["Title"],
                                Url = LinkManager.GetItemUrl(item)
                            };

        return categories;
    }
}
public class EntryCategoriesRenderingModel : RenderingModel
{
    private IEntryTaxonomy _entryTaxonomy = null;

    public IEnumerable<Category> Categories { get; set; }

    public EntryCategoriesRenderingModel(IEntryTaxonomy entryTaxonomy)
    {
        _entryTaxonomy = entryTaxonomy;
    }

    public override void Initialize(Rendering rendering)
    {
        base.Initialize(rendering);

        Categories = _entryTaxonomy.GetCategories(Item);
    }
}

Now that I'm passing an abstract IEntryTaxonomy instance into EntryCategoriesRenderingModel I can change it's implementation, even in the production code, without having to change the EntryCategoriesRenderingModel code. That allows me to switch out EntryTaxonomy with a different implementation of IEntryTaxonomy. Let's say I wanted to change to a taxonomy class which ordered the categories by name, or by popularity.

I've got a few more things I need to do to make this work in Sitecore. Firstly, I need to register EntryTaxonomy in the services registry so Sitecore DI can inject it. I'll do that through a configuration patch file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <services>
      <register
        serviceType="UnitTestingSitecoreComponents.Web.Taxonomy.IEntryTaxonomy,
          UnitTestingSitecoreComponents.Web"
        implementationType="UnitTestingSitecoreComponents.Web.Taxonomy.EntryTaxonomy,
          UnitTestingSitecoreComponents.Web" />
    </services>
  </sitecore>
</configuration>

The default Sitecore MVC model locator doesn't currently (as of Sitecore 9.3) support dependency injection, so I'll need to extend it to do so:

namespace UnitTestingSitecoreComponents.Web.Presentation
{
    public class ResolvingModelLocator : ModelLocator
    {
        protected override object GetModelFromTypeName(string typeName,
          string model, bool throwOnTypeCreationError)
        {
            var type = TypeHelper.GetType(typeName);
            var instance = ActivatorUtilities.CreateInstance(ServiceLocator.ServiceProvider, type);
            return instance;
        }
    }
}

And I'll need to replace the default model locator with the one above during the Initialize pipeline. I can do that with an initialize pipeline processor:

namespace UnitTestingSitecoreComponents.Web.Pipelines.Initialize
{
    public class RegisterModelLocator
    {
        public void Process(PipelineArgs args)
        {
            MvcSettings.RegisterObject<ModelLocator>(() => new ResolvingModelLocator());
        }
    }
}

Lastly, I'll register the RegisterModelLocator pipeline processor using a config patch file so it runs as part of the Initialize pipeline:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="UnitTestingSitecoreComponents.Web.Pipelines.Initialize.RegisterModelLocator,
          UnitTestingSitecoreComponents.Web" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Onto the Tests

I'm not going to worry about testing EntryTaxonomy just yet. That will be for a future post in this series. Those tests will be more complex because I'll need to mock Sitecore items and field values.

You may notice that IEntryTaxonomy doesn't require the use of Sitecore items, thus the tests for EntryCategoriesRenderingModel will be much simpler. All we have to do is mock IEntryTaxonomy with the behaviour we want to test.

I'll start with a simple parameter test to ensure the IEntryTaxonomy instance isn't null, as EntryCategoriesRenderingModel cannot operate without an instance of IEntryTaxonomy. For the tests I'll be using xUnit and nSubstitute.

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

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

Now to test the different behaviours I expect from calling GetCategories on IEntryTaxonomy. In these tests I'll be calling the Initialize method, which requires an instance of Rendering to be passed. If we don't populate the Item property of the rendering instance the base Initialize implementation will attempt to locate the context item, which will end up failing because we're not calling this method from inside a Sitecore web request. So I'll need to mock an item to satisfy it. I'll add the following utility method at the bottom of the test class.

private Item CreateItem()
{
    var database = Substitute.For<Database>();
    return Substitute.For<Item>(ID.NewID, ItemData.Empty, database);
}

Back to testing the Initialize method. In the first case, no categories are returned, so I expect the Categories property to be empty:

[Fact]
public void Initialize_NoCategories_CategoriesIsEmpty()
{
    // arrange
    var rendering = new Rendering();
    rendering.Item = CreateItem();
    var entryTaxonomy = Substitute.For<IEntryTaxonomy>();

    var sut = new EntryCategoriesRenderingModel(entryTaxonomy);

    // act
    sut.Initialize(rendering);

    // assert
    Assert.Empty(sut.Categories);
}

In the second case, categories are returned from IEntryTaxonomy, so I expect the Categories property to contain them:

[Fact]
public void Initialize_HasCategories_SetsCategories()
{
    // arrange
    var categories = new[]
    {
        new Category { Title = "cat1", Url = "link1" },
        new Category { Title = "cat2", Url = "link2" }
    };

    var rendering = new Rendering();
    rendering.Item = CreateItem();

    var entryTaxonomy = Substitute.For<IEntryTaxonomy>();
    entryTaxonomy.GetCategories(Arg.Any<Item>()).Returns(categories);

    var sut = new EntryCategoriesRenderingModel(entryTaxonomy);

    // act
    sut.Initialize(rendering);

    // assert
    Assert.Equal(categories, sut.Categories.ToArray());
}

Conclusion

At this point, the view, and the rendering model are in a good state, and even tested with minimal fuss. By encapsulating the business logic into a separate class that logic can be reused in other locations. By using dependency injection and abstracting the encapsulated logic, we've made it possible to substitute in different implementations of the business logic, whether they be real implementations that access Sitecore data, or they could be mocked implementations that return what we want for the purposes of a test.

In the next post we'll look at how to improve the EntryTaxonomy class further.

Comments

Leave a comment

All fields are required.