codeflood logo

Unit Testing Sitecore Components Part 3: Avoid Static Members

In the posts of this series so far, I’ve refactored a sample view rendering to make the rendering itself unit testable. In doing do I’ve extracted the business logic to a separate class which still needs some work before it can be properly tested. In this post, I’ll be covering 2 more principals to help with making Sitecore components more testable and reusable.

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 2: Encapsulate Logic
  • Principal: Encapsulate logic.
  • Principal: Use Dependency Injection and Abstractions

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

  • Principal: Avoid implicit data
  • Principal: Use Sitecore Abstractions

Avoid Implicit Data

Implicit data is data which the class accesses without it being passed into the class explicitly. A standard ASP.NET example of implicit data would be anything from the HttpContext like query string values or session state. A Sitecore example of implicit data would be anything from Sitecore.Context. This member is static and any class can easily access it without it being passed in.

The issue with using implicit data is that it tightly couples the code to that specific data, and accessing the data only in that way. I can’t reuse that code to work with data from a different source. If my code is accessing the context item through the Sitecore.Context.Item property, then I can’t then use that same code to work with items which I read from the content tree, or which are returned from Content Search.

Let’s take a look at an example.

ItemTitle.cs
1
2
3
4
5
6
7
8
9
public string GetItemTitle()
{
var item = Sitecore.Context.Item;
var title = item["title"];
if(string.IsNullOrEmpty(title))
title = item.Name;

return title;
}

The above code is tightly coupled with the Sitecore.Context class and it can only provide the title of the context item. This also makes testing the code more difficult, as I now need to set the Sitecore.Context.Item property to the item I want to use in the test.

If I avoid the implicit data (the context item) and instead pass the item in, then this code becomes much more reusable and easier to test:

ItemTitle.cs
1
2
3
4
5
6
7
8
public string GetItemTitle(Item item)
{
var title = item["title"];
if(string.IsNullOrEmpty(title))
title = item.Name;

return title;
}

If you’ve read the previous posts in this series, you’d note that the EntryTaxonomy class from the examples already avoids implicit data. In case you’ve not read those posts, here’s the current implementation of that class:

EntryTaxonomy.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

The item to retrieve the categories for is already being passed in as a parameter. This is a side-effect of the original example rendering being a View Rendering. View Renderings in Sitecore already help with breaking this anti-pattern by providing an Item property on the RenderingModel which is set either by the context item or the specified data source.

But if I’m avoiding implicit data, how do I determine the context item? Like all rules, there are exceptions. And in this case, the exception would be the entry points to your code. Constrain usage of implicit data to the entry points only. Keep your web entry points focused on pure web concerns; parsing requests, calling out to the appropriate isolated logic and preparing a web friendly response. Using Sitecore.Context.Item or even HttpContext.Current.Request.QueryString is perfectly fine inside a RenderingModel derivative or a controller. This data is concerned with the current request so can terminate at these entry points, but don’t let it leak beyond.

Use Sitecore Abstractions

There is one last refactoring I need to make to the EntryTaxonomy class before it’s ready to be tested. Currently, the class is using the static LinkManager.GetItemUrl() method. In many other systems the use of static members would render this code very difficult, if not impossible to test. This is because I can’t switch out the real implementation for a mock which I can easily validate. Sitecore does allow for switching the concrete LinkProvider implementation which the LinkManager calls, but it’s more complex than it needs to be.

Instead, we’ll use the available Sitecore abstractions of the manager classes along with dependency injection to pass in the link manager implementation:

EntryTaxonomy.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EntryTaxonomy : IEntryTaxonomy
{
private BaseLinkManager _linkManager;

public EntryTaxonomy(BaseLinkManager linkManager)
{
_linkManager = linkManager;
}

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

Now I am able to pass in a mocked BaseLinkManager in my unit tests and control it’s behaviour. And to make this work with the EntryCategoriesRenderingModel class from the previous post examples I don’t have to do anything. Sitecore dependency injection doesn’t jsut take care of the immediate dependencies into my code, it also handles dependencies for the dependencies. So it will pass the registered BaseLinkManager implementation when it instantiates my class.

Conclusion

Avoiding implicit data makes code more reusable and makes testing it much easier. By using Sitecore Abstractions rather than convenience static methods, I’ve prepared the code for testing.

With the final refactorings now in place, I’m ready to write the tests for the EntryTaxonomy class, which I’ll do in the next post.

Comments

Comments are closed

loading...

Leave a comment

All fields are required.