Background
Consider the following simple method that returns the names of the items of a content area:
public class ContentAreaHelper { public static IEnumerable<string> GetItemNames(ContentArea contentArea) { return contentArea.Items.Select(item => item.GetContent().Name); } }
The method is made up to be as simple as possible to illustrate the how unit testing against a ContentArea can be done, which turns out to be non-trivial.
You might often have more complex logic that really needs to be unit tested, but I’d argue that even simple methods like these are entitled to a few unit tests. Rather than just describe a finished solution, this post describes how to do this step by step and also includes some error messages you’re likely to encounter. The idea is to make it easier to find the post when googling for the subject matter or problems you might have. (For the record, this code has been tested with EPiServer 7.19, for other versions some adjustments may be required.)
To get started, let’s assume that we want to create a simple test, something like this:
[Test] public void GetNames_WithContentAreaItem_ShouldReturnTheName() { // Arrange var contentReference = new ContentReference(1); var content = new BasicContent { Name = "a" }; // TODO: Associate GetContent calls to content var contentArea = new ContentArea(); contentArea.Items.Add(new ContentAreaItem {ContentLink = contentReference}); // Act var names = ContentAreaHelper.GetItemNames(contentArea); // Assert Assert.That(names.Count(), Is.EqualTo(1)); Assert.That(names.First(), Is.EqualTo("a")); }
However, this is not a finished test since we don’t connect the content variable to be returned from the call to item.GetContent() in the ContentAreaHelper.GetItemNames() method. So how do we do that?
Well it’s not immediately obvious but if you dig into the GetContent() method you’ll find that it’s an extension method that retrieves an instance of IContentRepository from the ServiceLocator and then calls an overloaded extension method that takes an IContentRepository as parameter. That repository’s TryGet method is then called, so that’s where we need to hook up our content.
So first we need a IContentRepository that returns our content when queried. The easiest way to do that is by using a mocking framework, in this example I’m using FakeItEasy.
var contentRepository = A.Fake<IContentRepository>(); IContent outContent; A.CallTo(() => contentRepository.TryGet(contentReference, A<ILanguageSelector>.Ignored, out outContent)) .Returns(true) .AssignsOutAndRefParameters(content);
This basically tells FakeItEasy that we need a IContentRepository instance that returns our content when queried for the given content reference. EPiServer also passes a ILanguageSelector object but we’re not interested in its value so we ignore that parameter. The code is further complicated by the fact that TryGet is a method with an output parameter.
We’re not finished yet, but if you’re tempted and try to run the test right now, you’ll probably get an exception such as this:
EPiServer.ServiceLocation.ActivationException : Activation error occurred while trying to get instance of type ISecuredFragmentMarkupGeneratorFactory, key "" ----> StructureMap.StructureMapException : StructureMap Exception Code: 202 No Default Instance defined for PluginFamily EPiServer.Core.Html.StringParsing.ISecuredFragmentMarkupGeneratorFactory, EPiServer, Version=7.19.2.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7
This might seem very cryptic but basically the problem is that EPiServer’s service locator is not set up which is needed when adding items to a content area, for some obscure reason. So we now have two options, either we do just that, or we take a short cut. I like short cuts, so let’s try that first.
A shortcut – faking a ContentArea
As I mentioned earlier the GetContent() extension method has an overload that takes a IContentRepository repository as parameter. We could use that instead of GetContent and our test should work. So we rewrite the class under test like this:
public class ContentAreaHelper { public static IEnumerable<string> GetItemNames(ContentArea contentArea) { var rep = ServiceLocator.Current.GetInstance<IContentRepository>(); return GetItemNames(contentArea, rep); } public static IEnumerable<string> GetItemNames(ContentArea contentArea, IContentRepository rep) { return contentArea.Items.Select(item => item.GetContent(rep).Name); } }
And then we can change our test to call the second overload, passing in our fake IContentRepository. However, if we run the test now, which feels it should work, it still gives the ActivationException mentioned above. It occurs when adding items to the ContentArea as EPiServer apparently needs a live datasource for this to work. This is utterly confusing of course, and it might seem that the short cut is doomed. Not so!
Here comes the next trick. We don’t really need a “real” content area for the test, all we need is an object that looks like a ContentArea and behaves like it. If it looks like a duck, and all that. 🙂
So what we can do is to fake a ContentArea object and define ourselves what the Items collection contains:
//var contentArea = new ContentArea(); //contentArea.Items.Add(new ContentAreaItem {ContentLink = contentReference}); var contentArea = A.Fake<ContentArea>(); A.CallTo(() => contentArea.Items).Returns(new List<ContentAreaItem> {contentAreaItem});
If we run the test now, we get a new error:
EPiServer.BaseLibrary.ClassFactoryException : ClassFactory not initialized
Most people will now give up on the $@& EPiServer framework deeming unit testing impossible, but since we’re not quitters we add this to our test:
// Avoid "EPiServer.BaseLibrary.ClassFactoryException : ClassFactory not // initialized" EPiServer.BaseLibrary.ClassFactory.Instance = new EPiServer.Implementation.DefaultBaseLibraryFactory(string.Empty); EPiServer.BaseLibrary.ClassFactory.RegisterClass( typeof(EPiServer.BaseLibrary.IRuntimeCache), typeof(EPiServer.Implementation.DefaultRuntimeCache)); EPiServer.Globalization.ContentLanguage.PreferredCulture = new CultureInfo("en");
And finally, our test is green! This is what the final short cut version of the test looks like:
private IContentRepository _contentRepository; [SetUp] public void SetUp() { _contentRepository = A.Fake<IContentRepository>(); // Avoid "EPiServer.BaseLibrary.ClassFactoryException : ClassFactory not // initialized" ClassFactory.Instance = new DefaultBaseLibraryFactory(string.Empty); ClassFactory.RegisterClass(typeof(IRuntimeCache), typeof(DefaultRuntimeCache)); ContentLanguage.PreferredCulture = new CultureInfo("en"); } [Test] public void GetNames_WithContentAreaItem_ShouldReturnTheName() { // Arrange var contentReference = new ContentReference(1); var content = new BasicContent { Name = "a" }; // Create fake IContentRepository that returns our content IContent outContent; A.CallTo(() => _contentRepository.TryGet(contentReference, A<ILanguageSelector>.Ignored, out outContent)) .Returns(true) .AssignsOutAndRefParameters(content); // Create fake ContentArea with a ContentAreaItem var contentAreaItem = new ContentAreaItem { ContentLink = contentReference }; var contentArea = A.Fake<ContentArea>(); A.CallTo(() => contentArea.Items) .Returns(new List<ContentAreaItem> { contentAreaItem }); // Act var names = ContentAreaHelper.GetItemNames(contentArea, _contentRepository); // Assert Assert.That(names.Count(), Is.EqualTo(1)); Assert.That(names.First(), Is.EqualTo("a")); }
I moved parts of the code into a SetUp method that NUnit executes prior to each test to make the actual test method a little more clean, but it still isn’t very pretty. Extracting some of the setup into some helper methods is probably a good idea, but for brevity we’ll leave it like it is.
Ok, that was the shortcut version with a fake ContentArea, but what if we don’t want to rewrite our method to take a IContentRepository parameter? Or perhaps we’re writing tests against other methods that don’t have these handy overloads? Well, then we need to setup up a basic service locator registry and initialize the EPiServer framework’s ServiceLocator prior to the test.
Running the test with a configured ServiceLocator
Ok, time to go back to our original method under test:
public class ContentAreaHelper { public static IEnumerable<string> GetItemNames(ContentArea contentArea) { return contentArea.Items.Select(item => item.GetContent().Name); } }
And for the test, remember that we had created a IContentRepository fake that we want EPiServer to use. This is how we create a StructureMap object factory and tell EPiServer to use it for its ServiceLocator:
// Clear the StructureMap registry and reinitialize ObjectFactory.Initialize(expr => { }); ObjectFactory.Configure(expr => expr.For<IContentRepository>() .Use(contentRepository)); ObjectFactory.Configure(expr => expr.For<ISecuredFragmentMarkupGeneratorFactory>() .Use(A.Fake<ISecuredFragmentMarkupGeneratorFactory>())); ObjectFactory.Configure(expr => expr.For<IContentTypeRepository>() .Use(A.Fake<IContentTypeRepository>())); ObjectFactory.Configure(expr => expr.For<IPublishedStateAssessor>() .Use(A.Fake<IPublishedStateAssessor>())); // Set up the EPiServer service locator with our fakes ServiceLocator.SetLocator( new StructureMapServiceLocator(ObjectFactory.Container));
The fakes for ISecuredFragmentMarkupGeneratorFactory, IContentTypeRepository and IPublishedStateAssessor were added because StructureMap complained that it did not know where to find instances for those interfaces when running the tests.
We still get the “ClassFactory not initialized” exception as above so we must apply the same fix again. After that, the test works.
After some refactoring, this is what the test looks like:
private IContentRepository _contentRepository; [SetUp] public void SetUp() { _contentRepository = A.Fake<IContentRepository>(); // Clear the StructureMap registry and reinitialize ObjectFactory.Initialize(expr => { }); ObjectFactory.Configure(expr => expr.For<IContentRepository>() .Use(_contentRepository)); ObjectFactory.Configure(expr => expr.For<ISecuredFragmentMarkupGeneratorFactory>() .Use(A.Fake<ISecuredFragmentMarkupGeneratorFactory>())); ObjectFactory.Configure(expr => expr.For<IContentTypeRepository>() .Use(A.Fake<IContentTypeRepository>())); ObjectFactory.Configure(expr => expr.For<IPublishedStateAssessor>() .Use(A.Fake<IPublishedStateAssessor>())); // Set up the EPiServer service locator with our fakes ServiceLocator.SetLocator( new StructureMapServiceLocator(ObjectFactory.Container)); // Avoid "EPiServer.BaseLibrary.ClassFactoryException : ClassFactory not // initialized" ClassFactory.Instance = new DefaultBaseLibraryFactory(string.Empty); ClassFactory.RegisterClass(typeof(IRuntimeCache), typeof(DefaultRuntimeCache)); ContentLanguage.PreferredCulture = new CultureInfo("en"); } [Test] public void GetNames_WithContentAreaItem_ShouldReturnTheName() { // Arrange var contentReference = new ContentReference(1); var content = new BasicContent { Name = "a" }; // Associate GetContent calls to 'content' IContent outContent; A.CallTo(() => _contentRepository.TryGet(contentReference, A<ILanguageSelector>.Ignored, out outContent)) .Returns(true) .AssignsOutAndRefParameters(content); var contentArea = new ContentArea(); contentArea.Items.Add(new ContentAreaItem {ContentLink = contentReference}); // Act var names = ContentAreaHelper.GetItemNames(contentArea); // Assert Assert.That(names.Count(), Is.EqualTo(1)); Assert.That(names.First(), Is.EqualTo("a")); }
As before we do general initialization in the SetUp method and only do test-specific stuff in the actual test. This is so we can reuse as much setup as possible for the next test.
Final thoughts
So there you go, two ways of writing unit tests against an EPiServer ContentArea. Use the one most suitable for you. I tend to like the “faked ContentArea” version since you don’t have to get quite as messy with EPiServer internals, but sometimes it is not enough and I then use the other one. It’s useful to have both in your toolbox, and now you do as well. 🙂
There are probably other ways of accomplishing the same task, so feel free to comment below if you have opinions!
Cheers,
Emil