codeflood logo

Unit Testing Sitecore Components Part 4: Mocking Items and Fields

In the previous posts of this series, I've refactored an existing Sitecore component to make it's logic more reusable and prepare the component and the logic it includes for unit testing. In this post I'll be writing the unit tests for the refactored EntryTaxonomy class, showing how to mock items and field values.

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

Unit Testing Sitecore Components Part 3: Avoid Static Members

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

Unit Testing Sitecore Components Part 5: Recap and Resources

  • Recap
  • Resources

In this post, I'll be showing:

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

Mocking an Item

I've already shown how to mock an item in part 2 of this series: Unit Testing Sitecore Components Part 2: Encapsulate Logic. To save repeating the mocking code over and over, I encapsulated the code in a method on the test class. Here's the method for reference:

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

The class I need to test is the EntryTaxonomy class which was completed in part 3 of this series: Unit Testing Sitecore Components Part 3: Avoid Static Members. Here's the class for reference:

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

Only having the ability to mock Item won't get me very far in testing the GetCategories method beyond checking the guard clauses. And you might notice that I don't have any of those, so I'll start there:

public class EntryTaxonomyTests
{
    [Fact]
    public void Ctor_LinkManagerIsNull_Throws()
    {
        // arrange
        Action sutAction = () => new EntryTaxonomy(null);

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

    [Fact]
    public void GetCategories_ItemIsNull_ReturnsEmpty()
    {
        // arrange
        var linkManager = Substitute.For<BaseLinkManager>();
        var sut = new EntryTaxonomy(linkManager);

        // act
        var results = sut.GetCategories(null);

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

Both those tests will fail, because I'm currently missing guard checks for the parameters. I'll update EntryTaxonomy to validate the parameters:

public class EntryTaxonomy : IEntryTaxonomy
{
    private BaseLinkManager _linkManager;

    public EntryTaxonomy(BaseLinkManager linkManager)
    {
        if (linkManager == null)
            throw new ArgumentNullException(nameof(linkManager));

        _linkManager = linkManager;
    }

    public IEnumerable<Category> GetCategories(Item item)
    {
        if (entryItem == null)
            return Enumerable.Empty<Category>();

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

Inspecting the code for the GetCategories method I can see a couple of tests I want to write:

  • Item doesn't reference any categories
  • Item references 1 or more categories

But the current CreateItem method I have is quite limited and can't mock field values. So I'll fix that...

Mock Item Field Values

You'll see in the EntryTaxonomy class above I'm accessing the Category field through the Fields property of the Item. This is because I want to access the field as a MultilistField instance and not just get the string value of the field, which I would get if I used the indexer on the Item instance. The Fields property is of type FieldCollection. Your first instinct may be to simply instantiate a new FieldCollection instance for use in the test. But FieldCollection is not just a simple collection class, and it will use the static Sitecore methods to resolve things like templates when accessing fields, so it will be easier for the test if I mock this class as well.

private Item CreateItem()
{
    var db = Substitute.For<Database>();

    var item = Substitute.For<Item>(ID.NewID, ItemData.Empty, db);
    var fields = Substitute.For<FieldCollection>(item);
    item.Fields.Returns(fields);

    return item;
}

With this updated method I can now write the first test case from above, to validate when the item doesn't reference any categories.

public class EntryTaxonomyTests
{
    // other existing tests

    [Fact]
    public void GetCategories_NoCategories_ReturnsEmpty()
    {
        // arrange
        var entryItem = CreateItem();

        var linkManager = Substitute.For<BaseLinkManager>();
        var sut = new EntryTaxonomy(linkManager);

        // act
        var results = sut.GetCategories(entryItem);

        // assert
        Assert.Empty(results);
    }

    private Item CreateItem()
    {
        var item = Substitute.For<Item>(ID.NewID, ItemData.Empty, db);
        var fields = Substitute.For<FieldCollection>(item);
        item.Fields.Returns(fields);

        return item;
    }
}

The next test case requires I set the value of the Category field on the item. To do so, all I need to do is mock the field and add it to the existing fields mock instance. Rather than bundling all that into the existing method, I'll create a new test utility method to mock the field and add it.

private void SetItemField(Item item, string fieldName, string fieldValue)
{
    var field = Substitute.For<Field>(ID.NewID, item);
    field.Database.Returns(item.Database);
    field.Value = fieldValue;
    item.Fields[fieldName].Returns(field);
}

A multilist field in Sitecore is a pipe separate list of IDs of the items being referenced. As part of the "arrange" part of the test I'll also need to create the category items. You'll notice in the implementation of the GetCategories method that I'm using the GetItems method on the field instance, which resolves the items using the Database property of the field instance. So to be able to make sure my mocked items are returned, I'll need to mock the Database property of the field instance and set it up to return any of the items which I've mocked. That will require a few updates to the CreateItem test utility methods.

private Item CreateItem(Database database = null)
{
    var db = database ?? Substitute.For<Database>();

    var item = Substitute.For<Item>(ID.NewID, ItemData.Empty, db);
    var fields = Substitute.For<FieldCollection>(item);
    item.Fields.Returns(fields);

    db.GetItem(item.ID).Returns(item);
    db.GetItem(item.ID.ToString()).Returns(item);

    return item;
}

I'm almost ready to write that last test case. But I have one more update to make before I can. Looking over the GetCategories method, the last thing I'll need to be able to do is use the item indexer to access the Title field of the mocked category item instances.

private void SetItemField(Item item, string fieldName, string fieldValue)
{
    item[fieldName].Returns(fieldValue);

    var field = Substitute.For<Field>(ID.NewID, item);
    field.Database.Returns(item.Database);
    field.Value = fieldValue;
    item.Fields[fieldName].Returns(field);
}

Now I'm ready to write that last test.

public class EntryTaxonomyTests
{
    // other existing tests

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

        var cat1 = CreateItem(database);
        SetItemField(cat1, "Title", "cat1");

        var cat2 = CreateItem(database);
        SetItemField(cat2, "Title", "cat2");

        var entryItem = CreateItem(database);
        SetItemField(entryItem, "Category", $"{cat1.ID}|{cat2.ID}");

        var linkManager = Substitute.For<BaseLinkManager>();
        linkManager.GetItemUrl(cat1).Returns("link1");
        linkManager.GetItemUrl(cat2).Returns("link2");

        var categories = new[]
        {
            new Category { Title = "cat1", Url = "link1" },
            new Category { Title = "cat2", Url = "link2" }
        };

        var sut = new EntryTaxonomy(linkManager);

        // act
        var results = sut.GetCategories(entryItem);

        // assert
        Assert.Equal(categories, results.ToArray(), new CategoryComparer());
    }

    // test utility methods from above
}

Conclusion

And there we have it! The EntryTaxonomy class is now fully unit tested by mocking the Sitecore data classes. It's nice and lightweight and doesn't require a running Sitecore instance to run.

Comments

Leave a comment

All fields are required.