Mocking Sitecore has been a tough nut to crack since Sitecore lacks the necessary interfaces that common mocking frameworks need, which really screws up automated testing for a lot of us. For that we turn to isolation frameworks like TypeMock and Justmock which unfortunately come with a huge price tag. Thankfully Microsoft Fakes is here to provide an alternative that may not be everyone’s cup of tea but does the job alright. If you’re using Visual Studio Premium, Ultimate, or Enterprise then you’re in for a treat because MS Fakes is already included in these versions for no extra cost.
Below are functions that can ramp you up with faking Sitecore item, template, and field collections, but for better understanding on how MS Fakes works I’d recommend reading the official Microsoft blog first: https://msdn.microsoft.com/en-us/library/hh549175.aspx
Faking the Item
private readonly Language ContextLanguage = Language.Parse("en");
ShimItem CreateFakeItem(ShimItem parentItem, string name, Action<ShimItem, ShimTemplateItem, List<ShimField>> onItemCreating)
{
var id = ID.NewID;
var item = new ShimItem()
{
// Assigning ID.NewID directly will return a new ID every time item.ID is called
IDGet = () => id,
KeyGet = () => name.ToLower(),
NameGet = () => name,
HasChildrenGet = () => false,
ParentGet = () => parentItem,
PathsGet = () =>
{
var path = (parentItem != null ? parentItem.Instance.Paths.Path : "") + "/" + name;
return new ShimItemPath()
{
PathGet = () => path,
FullPathGet = () => path,
};
},
LanguageGet = () => ContextLanguage,
VersionsGet = () => new ShimItemVersions() { CountGet = () => { return 1; } }
};
// Bind item to parent item
if (parentItem != null)
{
var children = parentItem.Instance.HasChildren ? parentItem.Instance.Children.ToList() : new List<Item>();
children.Add(item);
parentItem.HasChildrenGet = () => true;
parentItem.ChildrenGet = () => new ChildList(parentItem.Instance, children);
parentItem.GetChildren = () => parentItem.Instance.Children;
}
// Start faking template and field collection
var templateItem = new ShimTemplateItem();
var fields = new List<ShimField>();
// Call action to allow extension of item, template, and field collection faking
onItemCreating(item, templateItem, fields);
item.TemplateGet = () => templateItem;
item.FieldsGet = () => CreateFakeFieldCollection(item, fields);
return item;
}
Faking the Field Collection
// Create a dictionary to hold the field collection per item ID
private Dictionary<ID, List<ShimField>> itemFields = new Dictionary<ID, List<ShimField>>();
ShimFieldCollection CreateFakeFieldCollection(ShimItem item, List<ShimField> fields)
{
foreach (var field in fields)
field.ItemGet = () => item;
var fieldCollection = new ShimFieldCollection()
{
ItemGetString = (fieldName) =>
{
return fields.SingleOrDefault(n => n.Instance.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
}
};
if (!itemFields.ContainsKey(item.Instance.ID))
itemFields.Add(item.Instance.ID, fields);
else
itemFields[item.Instance.ID] = fields;
fieldCollection.Bind(itemFields[item.Instance.ID]);
return fieldCollection;
}
Faking the Sitecore Context
Normally a Sitecore item is retrieved using Sitecore.Context.Database.GetItem function, therefore we’re gonna need to fake the Context.
void FakeSitecoreContext()
{
ShimContext.LanguageGet = () => ContextLanguage;
ShimContext.SiteGet = () => new ShimSiteContext()
{
ContentLanguageGet = () => ContextLanguage
};
Func<Func<Item, bool>, Item> getItem = (predicate) =>
{
Item result;
return TryGetItem(this.Sitecore.Instance.Children, predicate, out result) ? result : null;
};
ShimContext.DatabaseGet = () => new ShimDatabase()
{
GetItemString = (path) => getItem(n => n.Paths.Path.Equals(path, StringComparison.OrdinalIgnoreCase)),
GetItemStringLanguage = (path, lang) => getItem(n => n.Paths.Path.Equals(path) && (n.Language.Equals(lang) || n.Languages != null && n.Languages.Any(l => l.Name.Equals(lang.Name)))),
GetItemID = (id) => getItem(n => n.ID.Equals(id)),
GetItemIDLanguage = (id, lang) => getItem(n => n.ID.Equals(id) && (n.Language.Equals(lang) || n.Languages != null && n.Languages.Any(l => l.Name.Equals(lang.Name)))),
};
}
bool TryGetItem(ChildList children, Func<Item, bool> predicate, out Item result)
{
result = null;
if (children == null || !children.Any()) return false;
result = children.FirstOrDefault(predicate);
if (result != null) return true;
var query = children.Where(n => n.HasChildren);
if (!query.Any()) return false;
foreach (var child in query.ToArray())
{
if (TryGetItem(child.Children, predicate, out result))
return true;
}
return false;
}
Faking the Base Item
We are going to have to fake the Base Item class so that we can use the item’s indexed property and get the value of a field like in item[“Fieldname”].
void FakeBaseItem()
{
ShimBaseItem.AllInstances.ItemGetString = (baseItem, fieldName) =>
{
Item result;
TryGetItem(Sitecore.Instance.Children, (n) => object.Equals(baseItem, n), out result);
if (result != null)
{
var fields = itemFields[result.ID];
var field = fields.FirstOrDefault(n => n.Instance.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
if (field != null) return field.Instance.Value;
}
return string.Empty;
};
}
Faking the Sitecore tree
Now we can start faking the Sitecore tree and organize it accordingly.
public ShimItem Sitecore, Content, Site, Home;
public void Initialize(Action onInitializing = null)
{
Sitecore = CreateFakeItem(null, "sitecore", (sitecore) => {
Content = CreateFakeItem(sitecore, "content", (content) =>
{
Site = CreateFakeItem(content, "site", (site) =>
{
Home = CreateFakeItem(site, "home", (home) =>
{
// Add more items if you must to mimic your Sitecore tree
});
});
});
});
if (onInitializing != null)
onInitializing(this);
FakeBaseItem();
FakeSitecoreContext();
}
Of course a CreateFakeItem overload is necessary for this to work:
public ShimItem CreateFakeItem(ShimItem parentItem, string name, Action onItemCreating)
{
return CreateFakeItem(parentItem, name, (i, t, f) =>
{
if (onItemCreating != null)
onItemCreating(i);
});
}
The Sample Test
Shimming requires that you create your shim objects inside a using statement that has a disposable ShimsContext. Instead of doing this I personally like to create a ShimsContext object during the test initialization and destroy it during the test cleanup. It works the same way as with the using statement but with a lot less coding.
[TestClass]
public class SampletTests
{
IDisposable _context;
readonly SitecoreFaker _scFaker = new SitecoreFaker();
[TestInitialize]
public void Initialize()
{
_context = ShimsContext.Create();
_scFaker.Initialize();
}
[TestCleanup]
public void Cleanup()
{
_context.Dispose();
}
[TestMethod]
public void Sample_Test()
{
// Arrange:
var expectedItem = _scFaker.CreateFakeItem(_scFaker.Home, "sample item");
// Act:
var actualItem = Context.Database.GetItem(expectedItem.Instance.Paths.FullPath);
// Assert:
Assert.AreEqual(expectedItem, actualItem);
}
}
I find it really handy to use this functionalities for all my Sitecore related unit tests. The only thing that I am not really happy about is that I always have to use Shim objects which Microsoft has documented to degrade the testing performance. Running the tests using vstest.console.exe makes up for it though.
Hi Jeff,
Your Blog Post has been a major Life saver as i have shaped my entire Unit Testing solution around this awesome piece of code.But i have ran into a bit of a roadblock when i try to access the fake Items from my unit tests. I have put up a detailed question regarding my blockage here:
https://stackoverflow.com/questions/46618714/not-able-to-access-sitecore-mock-items-ms-fakes-through-sitecore-context-in-un
Please answer if time allows for it.
Hi Nitin,
I’ve replied to your question on stackoverflow.
Let’s continue solving your problem there.
Cheers,
Jeff
HI,
Very Informative and interesting Post. Just what i need to Unit Test Sitecore but one question though…You have always defined List() as a Method and nowhere as List…this is throwing an error when i am trying to implement your solution in my Project. Can you explain this to me….? Pardon my lack of Knowledge as i am new to both Sitecore and Unit Testing. A quick prompt reply would be a lifesaver.
Thanks,
Nitin
Hi Nitin,
Apologies for this inconvenience and thanks for pointing out the errors in my code. I have since corrected it, so I hope this solves your build issues. Let me know if there’s anything else I can help with. Cheers!