codeflood logo

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

At the end of April (2020), I had the great pleasure of presenting at the virtual SUGCON conference online. My presentation was a live walkthrough showing how to unit test a Sitecore component. In the presentation I utilised many principals to refactor and improve a view rendering.

The first issue most developers will face when unit testing Sitecore components is the need to mock Sitecore items. But I wanted to show a more holistic approach to unit testing, and show how component design and architecture can impact the testability, reusability and flexbility of the component. I wanted to go beyond just showing how to mock an item.

So I decided to create a series of blog posts so I could go over each of the principals I applied in the presentation in more detail, starting with this one. In this post I'll cover the first 2 principals, as those are intrinsically linked in the examples I run through.

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

Other posts in this series and the princiapls they cover:

Unit Testing Sitecore Components Part 2: Encapsulate Logic

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

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

Example Rendering

The following is an example view rendering which we'll analyse and refactor during this series. I created this rendering as an alternative to an existing rendering in the WeBlog blog module for Sitecore. You'll not find this rendering in the project source as I created it purely to support my presentation.

It's a faily simple rendering which outputs the categories the current blog entry has been assigned to. As simple as it looks, it's not in a good shape for testability or reusability.

@using Sitecore.Links
@model UnitTestingSitecoreComponents.Web.RenderingModels.EntryCategoriesRenderingModel

@if(Model.CategoryItems.Any())
{
    <div class="wb-entry-categories wb-panel">
        <h3>(View Rendering) Posted in:</h3>
        <ul>
            @foreach (var categoryItem in @Model.CategoryItems)
            {
                <li>
                    <a href="@LinkManager.GetItemUrl(categoryItem)">
                        @Html.Sitecore().Field("Title", categoryItem)
                    </a>
                </li>
            }
        </ul>
    </div>
}

And the model used with the view rendering:

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Mvc.Presentation;
using System.Collections.Generic;
using System.Linq;

namespace UnitTestingSitecoreComponents.Web.RenderingModels
{
    public class EntryCategoriesRenderingModel : RenderingModel
    {
        public IEnumerable<Item> CategoryItems { get; set; }

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

            var categoryField = (MultilistField)Item.Fields["Category"];
            CategoryItems = categoryField?.GetItems() ?? Enumerable.Empty<Item>();
        }
    }
}

Keep business logic out of the views

Many developers strive for logic-less views. In fact, some popular templating engines such as Mustache are built on top of this very principal. There are a few issues with having logic in your view. The first is testability. To test any logic in a view, the view would have to be executed, which would generally turn the test into an integration test. There's nothing wrong with integration tests; in fact any project needs a good mix of both unit and integration tests (and other higher order tests). But we'd be missing out on the quick feedback and cost savings we get from unit tests. As we move up the Test Pyramid tests take longer to run and bugs caught are more expensive to fix than if they'd been caught in a lower level.

Another issue is the spreading out of the logic between multiple files. There would be some logic required in preparing the model passed to the view and by also including logic in the view the full logic to construct the data shown on the view is spread between multiple locations. This makes discoverability and maintenance more difficult as developers now need to jump around multiple files to get a full picture of the logic. Overall, it defeats a clear separation of concerns.

Keep Item out of the model

In a test context we can easily mock an item (and I'll walk through that process again in this series) but in the production code items must be loaded from the content tree. We cannot create an item any other way. That means we cannot create the full model from any other source such as content search or even an external system. This might not appear to be such an issue for a component which only uses a single item and only requires a single item to be loaded from the content tree, but that can cause major performance problems if you need lots of items from the content tree. I recently wrote about this exact issue in Keep items out of your model.

Consider a deep hierarchical navigation component which provides navigation from the second level of pages right down to the bottom. This component will access many items to render. If we use Item in the model, we'd need to load all those items from the content tree.

Refactoring

We'll apply both these principals at the same time.

To remove the business logic from the view we'll need to move it somewhere else. The model we pass to the view will need to contain all the output data from the business logic. Inspecting the view we can see two pieces of business logic.

Firstly, we generate a link using the LinkManager class. And secondly, the title of the category item is output from a field. We may be starting to encroach on a grey area as to whether this is considered business logic or not. I think it is, because there may be many rules which must be applied in generating these two pieces of data. What if we needed to add a rule around the category title? What if we wanted to output the name of the item if the Title field is empty? That there is squarly business logic.

So the two pieces of data we need the model to hold instead of the category item is the URL for the category, and the title of the category. Let's create a new class to hold that data:

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

        public string Url { get; set; }
    }
}

Now let's update the rendering model to use this class and get rid of Item. To populate the Category instances we'll also pull in the logic from the view.

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)
                            };
    }
}

Now we can update the view to remove the logic and use the Category instances.

@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>
}

With these changes we've applied both principals.

Conclusion

There's still a lot more to do to improve this rendering. By moving the business logic out of the view we've made the logic easier to discover and maintain as it's all in one location now. And although we don't yet have any unit tests, we've taken the first steps towards being able to test the logic. Through virtue of moving the business logic, we also removed Item from the model, so now the model can easily be instantiated from any context and not only by accessing the Sitecore data API. The categories could for example be instanitated directly out of content search making the rendering perform much better.

We'll continue to improve the rendering as we explore additional principals in the next post of this series: Unit Testing Sitecore Components Part 2: Encapsulate Logic.

Comments

Leave a comment

All fields are required.