Advanced Unit Testing of Sitecore 9 with FakeDb and NSubstitute


Intro

This blog post assumes the reader has general knowledge of unit testing methodology, Sitecore 9, FakeDb, and NSubstitute.  The documentation for FakeDb provides some simple examples.  The scope of this post is to expand upon those examples and provide a quick reference to common unit testing scenarios with Sitecore 9.

Setup

Even though this post doesn’t contain a FakeDb installation walk-through, it is worth noting that for Sitecore 8.2 and above, the app.config file of your unit testing project with FakeDb installed must set the databaseType variable to a value for the Sitecore.Data.DefaultDatabase rather  than Sitecore.Data.Database.

<sc.variable name="databaseType" value="Sitecore.Data.DefaultDatabase, Sitecore.Kernel" />

Failing to set this value correctly will result in the following Exception:

Sitecore.Exceptions.ConfigurationException: Could not create instance of type: Sitecore.Data.Database. No matching constructor was found.

Some of the examples use references to nuget packages.  Here is a list of the packages that may need to be referenced in order to execute example code besides FakeDb and NSubstitute:

  • Microsoft.Extensions.DependencyInjection
  • Sitecore.DependencyInjection
  • Sitecore.Mvc

Mocking Built-In Sitecore Nodes

One common unit testing scenario involves testing code that expects the built-in structure of Sitecore.  The built-in nodes are sometimes considered starting points for custom items placed within these nodes.  As such, the unit testing code will sometimes need to mock the built-in Sitecore nodes. 

Using the ItemIDs class from the Sitecore.Kernel assembly in conjunction with FakeDb, we can easily mock most of this built-in structure.  Here are the examples:

Mock Layout Root

using (Db db = new Db()

{

new DbItem("Layout", ItemIDs.LayoutRoot, TemplateIDs.MainSection)

{

ParentID = ItemIDs.RootID,

FullPath = "/sitecore/layout",

Children =

{

new DbItem("Devices", ItemIDs.DevicesRoot, TemplateIDs.Node)

{

new DbItem("Default", deviceId, TemplateIDs.Device)

{

{DeviceFieldIDs.Default, "1"}

}

},

new DbItem("Layouts", ItemIDs.Layouts, TemplateIDs.Node) { },

new DbItem("Placeholder Settings", ItemIDs.PlaceholderSettingsRoot, TemplateIDs.Folder) { }

}

}

}) { }

Mock Media Library Root

using (Db db = new Db()

{

new DbItem("Media Library", ItemIDs.MediaLibraryRoot, TemplateIDs.MainSection) { }

}) { }

Mock System Root

using (Db db = new Db()

{

new DbItem("System", ItemIDs.SystemRoot, TemplateIDs.MainSection)

{

new DbItem("Dictionary", ItemIDs.Dictionary, TemplateIDs.Node) {},

new DbItem("Languages", ItemIDs.LanguageRoot, TemplateIDs.Node) {},

new DbItem("Workflows", ItemIDs.WorkflowRoot, TemplateIDs.Node) {}

}

}) { }

Mocking Common Field Types

Sitecore Out-of-the-Box (OOTB) ships with several built-in field types.  Another common unit testing scenario is to mock these field types when the code being tested expects template fields to be of a specific type.  Here are a few examples for common field types:

  • Link

    string itemLinkUrl = "~/testlinkitem";

    ID testItemId = ID.NewID;

    ID testLinkItemId = ID.NewID;

    ID linkFieldId = ID.NewID;

    using (Db db = new Db

    {

    new DbItem("TestLinkItem", testLinkItemId),

    new DbItem("TestItem", testItemId)

    {

    new DbLinkField(linkFieldId)

    {

    TargetID = testLinkItemId,

    LinkType = "internal"

    }

    }

    })

    {

    BaseLinkManager linkManager = Substitute.For();

    linkManager.GetItemUrl(Arg.Is(i => i.ID == testLinkItemId))

    .Returns(itemLinkUrl);

     

    BaseFieldTypeManager fieldTypeManager = Substitute.For();

    fieldTypeManager

    .GetField(Arg.Is(f => f.ID == linkFieldId), Arg.Any<string>())

    .Returns(args => new LinkField(args[0] as Field));

    fieldTypeManager

    .GetFieldType(Arg.Is<string>(v => v.ToLower() == "link"))

    .Returns(new FieldType("LinkField", typeof(LinkField), false, false));

    //TODO:  Add Manager mocks to DI container

    }

  • Image (Media)

    var imageUrl = "~/Test-Image";

    ID testItemId = ID.NewID;

    ID mediaItemId = ID.NewID;

    ID mediaFieldId = ID.NewID;

    using (Db db = new Db()

    {

    new DbItem("MediaSource", mediaItemId),

    new DbItem("TestItem", testItemId)

    {

    mediaFieldId, $"\"media\" mediaid=\"{mediaItemId.ToString()}\" height=\"50\" width=\"50\" />" }

    }

    })

    {

    MediaProvider mediaProvider = Substitute.For();

    mediaProvider

    .GetMediaUrl(Arg.Is(i => i.ID == mediaItemId), Arg.Any())

    .Returns(imageUrl);

    UrlHashProtector hashProtector = Substitute.For();

    hashProtector

    .ProtectMediaItemUrlWithHash(Arg.Any<string>())

    .Returns(callInfo => callInfo.ArgAt<string>(0));

    //TODO:  Add mocks to DI container

    }

  • Lookup

    ID testItemId = ID.NewID;

    ID referenceFieldId = ID.NewID;

    ID testTargetItemId = ID.NewID;

    using (Db db = new Db

    {

    new DbItem("TargetItem", testTargetItemId),

    new DbItem("TestItem", testItemId)

    {

    new DbField(referenceFieldId)

    {

    Type = "Lookup",

    Value = "/sitecore/content/targetitem"

    }

    }

    }) { }

  • Multilist

    ID testItemId = ID.NewID;

    ID referenceFieldId = ID.NewID;

    ID testTargetItemId = ID.NewID;

    ID testTargetItem2Id = ID.NewID;

    using (Db db = new Db()

    {

    new DbItem("TargetItem", testTargetItemId),

    new DbItem("TargetItem2", testTargetItem2Id),

    new DbItem("TestItem", testItemId)

    {

    new DbField(referenceFieldId)

    {

    Value = $"{testTargetItemId}|{testTargetItem2Id}"

    }

    }

    }) { }

  • Checkbox Field

    ID testItemId = ID.NewID;

    ID checkboxFieldId = ID.NewID;

    using (Db db = new Db()

    {

    new DbItem("TestItem", testItemId)

    {

    new DbField(checkboxFieldId)

    {

    Value = "1" //Use 0 for false

    }

    }

    }) { }

Mocking Page Visualization

A major function of Sitecore is to render an item’s html based on a customized visualization of the item.    Because Sitecore allows Content Editors to change an item’s visualization by default, there may be a need for application code to inspect an item’s rendering list using the item.Visualization.GetRenderings() method. 

In order to mock the call to the GetRenderings method so that it returns the renderings expected by the application code, the first thing to do is to mock the Page, Device, and Rendering items within the Sitecore structure.  Using code from the previous section “Mocking Built-In Sitecore Nodes”, start by setting up the mock database. 

ID deviceId = ID.NewID;

ID layoutId = ID.NewID;

using (Db db = new Db()

{

new DbItem("Layout", ItemIDs.LayoutRoot, TemplateIDs.MainSection)

{

ParentID = ItemIDs.RootID,

FullPath = "/sitecore/layout",

Children =

{

new DbItem("Devices", ItemIDs.DevicesRoot, TemplateIDs.Node)

{

new DbItem("Default", deviceId, TemplateIDs.Device)

{

{ DeviceFieldIDs.Default, "1" }

}

},

new DbItem("Layouts", ItemIDs.Layouts, TemplateIDs.Node)

{

new DbItem("Default", layoutId, TemplateIDs.Layout)

}

}

}

})

Now that the Sitecore structure is setup in the mock database, mock the layout field value using xml.

var templateLayout =

$@" ""http://www.w3.org/2001/XMLSchema"">

""{deviceId}"" l=""{layoutId}"" />

";

 

ID renderingItemId = ID.NewID;

ID renderingUniqueItemId = ID.NewID;

 

var itemDelta =

$@" ""p"" xmlns:s=""s"" p:p=""1"">

""{deviceId}"">

""{renderingUniqueItemId}"" s:id=""{renderingItemId}"" s:ph=""Main"" />

";

 

var layout = XmlDeltas.ApplyDelta(templateLayout, itemDelta);

Now that the layout value is built, add the mock test page to the database with the layout field and value.

ID pageItemId = ID.NewID;

 

db.Add(new DbItem("test-page", pageItemId)

{

{ FieldIDs.LayoutField, layout }

});

With that, the layout for the test-page is setup.  The code under test for this example looks like this:

var db = Factory.GetDatabase("master");

var device = db.GetItem("/sitecore/layout/devices/default");

var renderings = db.GetItem("/sitecore/content/test-page").Visualization.GetRenderings(device, true);

Mocking Managers

With the initial release of Sitecore 8.2, the API was modified to ease mocking of API Manager classes by providing abstractions in the Sitecore.Abstractions namespace.  Nearly every Manager class has a corresponding BaseManager abstract class in the Sitecore.Abstractions namespace that allows you to mock Manager classes that your application code depends on, so you can focus on testing only your application code.

Because we will be replacing built-in Sitecore services, the first thing we need to do is start with an empty ServiceCollection and add all of the default Sitecore registrations to it:

var serviceCollection = new ServiceCollection();

new DefaultSitecoreServicesConfigurator().Configure(services);

Next, create a mock Manager object, configure any method calls necessary for the test and then add it to our ServiceCollection.

BaseMediaManager mediaManager = Substitute.For();

//Configure method calls

//Example configuring the GetMediaUrl method to return imageUrl.

var imageUrl = "~/Test-Image";

mediaManager.GetMediaUrl(Arg.Is(i => i.ID == mediaItemId), Arg.Any())

.Returns(imageUrl);

serviceCollection.AddSingleton(mediaManager);

Lastly, the application code needs to be run under a context where the configured ServiceCollection will be used by the DI container.  In order to achieve this, the test code can be wrapped in the following code:

var scopeFactory = serviceCollection

.BuildServiceProvider()

.GetRequiredService();

 

using (var scope = scopeFactory.CreateScope())

{

var provider = scope.ServiceProvider;

ServiceLocator.SetServiceProvider(provider);

 

//Put your test code here (ACT & ASSERT)

}

Mocking the RenderingContext

Rendering code may refer to the RenderingContext in order to retrieve the values of rendering parameters or the current context item.  In order to test this code, there is a static method on the Sitecore.Mvc.Presentation.RenderingContext class called EnterContext.  This method can be used for mocking the RenderingContext that the application code expects.  Here is an example:

ID homeItemId = ID.NewID;

ID templateId = new ID(Templates.CaseStudyHome.IdString);

using (Db db = new Db("web")

{

new DbItem("Home Item", homeItemId, templateId)

})

{

Item homeItem = db.GetItem(homeItemId);

using (RenderingContext.EnterContext(new Rendering(), homeItem))

{

//Put your test code here (ACT & ASSERT)

}

}

Mocking the ControllerContext

While not necessarily specific to unit testing in Sitecore, it is useful to know how to mock the ControllerContext object for Controllers.  This allows for setting up query string values and other data the application code for a Controller Rendering may expect.  Here is an example of how to mock a ControllerContext:

HttpRequestBase request = Substitute.For();

HttpContextBase context = Substitute.For();

context.Request.Returns(request);

controller.ControllerContext = new ControllerContext(context, new RouteData(), controller);

Conclusion

Mocking Sitecore objects that application code depends on can be daunting, but hopefully this reference will make your journey through unit testing with Sitecore easier.

Categories: Unit Testing

SEARCH ARTICLES