Sitecore developers often need to reference content items from their code. E.g.: a template or branch ID for an import function or reading a global setting that’s stored in the content tree. Using IDs is usually preferred over using paths, because paths may change. It’s considered good practice to keep those ID references in a class with constants, like this:
1 2 3 4 5 |
public static class Constants { public static readonly ID SampleTemplateId = new ID("{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}"); } |
After recently contributing some serialization logic to Sitecore.FakeDb, I got the idea to write unit tests that validate if these constants have values that are actually in Sitecore. We can use TDS or Unicorn serialized data to validate this.
The following unit test does just that (uses NUnit, FluentAssertions, Sitecore.FakeDb and Sitecore.FakeDb.Serialization):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[Test(Description = "Checks if Sitecore IDs in a class with constants can be found in the serialized data")] public void ShouldIdsBeFoundInSerializedData( [Values(typeof(Constants))] Type typeWithIdConstants) { foreach (FieldInfo field in typeWithIdConstants .GetFields(BindingFlags.Public | BindingFlags.Static) .Where(f => f.FieldType == typeof (ID))) { ID constantValue = field.GetValue(null) as ID; if (((object) constantValue) != null) { constantValue .FindFilePath("master") .Should() .NotBeNullOrWhiteSpace( "Item with ID {0} (as defined by {1}.{2}) should be in the serialized data", constantValue, field.DeclaringType.Name, field.Name); } } } |
Just pass in the types (that have Sitecore IDs as static properties) using the “Values” attribute.
But I didn’t stop there. At my company, we’re currently working on a project that uses Glass Mapper. Glass Mapper can use attributes to link mapped classes to the correct Sitecore templates. And in a similar way, properties can be linked to field IDs.
1 2 3 4 5 6 |
[SitecoreType(TemplateId = "{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}")] public interface ISampleItem { [SitecoreField(FieldId = "{75577384-3C97-45DA-A847-81B00500E250}")] string Title { get; set; } } |
I wanted to validate all those IDs as well, and also check if the field IDs are on the right templates.
That’s why I wrote the following unit test. It finds all the types that have attributes like this and deserializes the corresponding templates. It also checks if the fields are there.
To use it, you need to pass in one type from the assembly that you want to test (or multiple) using the “Values” attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
[Test(Description = "Checks if all Glass mapper template and field IDs can be found in serialized data")] public void ShouldAllTemplatesAndFieldsBeFoundInSerializedData( [Values( typeof(SomeType), // From assembly MyProject )] Type typeFromAssembyUnderTest) { Assembly assemblyUnderTest = Assembly.GetAssembly(typeFromAssembyUnderTest); // Find all templates and fields that need to be checked in the serialized data List<ID> sitecoreTypes = GetAllTemplateIds(assemblyUnderTest.GetTypes()); List<KeyValuePair<ID,ID[]>> sitecoreFields = GetAllSitecoreFieldsFromAssembly(assemblyUnderTest); Console.WriteLine( "Now checking {0} template ids and {1} fields (from assembly: {2}) in serialized data", sitecoreTypes.Count, sitecoreFields.Count, assemblyUnderTest.GetName().Name); using (var db = new Db()) { // Find all the templates and deserialize them into the FakeDb List<DsDbTemplate> templates = sitecoreTypes.Select(t => new DsDbTemplate(t)).ToList(); foreach (DsDbTemplate template in templates) { db.Add(template); } // Iterate all the templates and check if they are correctly deserialized and are actual templates foreach (ID sitecoreType in sitecoreTypes) { Item item = db.GetItem(sitecoreType); // check if the template was actually deserialized item.Should().NotBeNull(); item.TemplateID.ShouldBeEquivalentTo(TemplateIDs.Template); item.Paths.FullPath.Should().StartWith("/sitecore/templates/"); } // Iterate all the fields and check if they are correctly deserialized and valid for the deserialized templates foreach (var sitecoreField in sitecoreFields) { // Check if the field is valid on the template(s) for the class that references it templates .Where(t => sitecoreField.Value.Contains(t.ID)) .Any(t => t.Fields.Any(f => f.ID == sitecoreField.Key)) .Should() .BeTrue( "Field with ID {0} should be available on any of the templates with ID's {1}", sitecoreField.Key, string.Join(", ", sitecoreField.Value.Select(v => v.ToString()))); } } } /// <summary> /// Find all template IDs that are defined on all of the types that are passed. /// </summary> /// <param name="types"></param> /// <returns></returns> private static List<ID> GetAllTemplateIds(IEnumerable<Type> types) { return (from t in types let attributes = t.GetCustomAttributes<SitecoreTypeAttribute>(true) where attributes != null && attributes.Any() select attributes) .SelectMany(a => a) .Where(a => a.TemplateId != null) .Select(a => ID.Parse(a.TemplateId)) .ToList(); } /// <summary> /// Finds all fields marked with the SitecoreFieldAttribute in the whole assembly. /// Returns a list of pairs where the key is the field ID and the value is an array of template IDs that the field might belong to. /// </summary> /// <param name="assembly"></param> /// <returns></returns> private static List<KeyValuePair<ID, ID[]>> GetAllSitecoreFieldsFromAssembly(Assembly assembly) { return assembly.GetTypes().SelectMany(t => t.GetProperties()) .Where(p => p.IsDefined(typeof (SitecoreFieldAttribute), true)) .Select(p => new { Type = p.ReflectedType, Attr = p.GetCustomAttribute<SitecoreFieldAttribute>(true) }) .Where(a => a.Attr.FieldId != null) .Select(a => new KeyValuePair<ID, ID[]>( ID.Parse(a.Attr.FieldId), GetAllTemplateIdsIncludingFromBaseTypes(a.Type).ToArray())) .ToList(); } /// <summary> /// Finds all of the template IDs that are declared on the type or any base class or interface that it implements. /// </summary> /// <param name="type"></param> /// <returns></returns> private static List<ID> GetAllTemplateIdsIncludingFromBaseTypes(Type type) { return GetAllTemplateIds(GetBaseTypes(type).Distinct().Concat(new[] {type})); } /// <summary> /// Finds all of the base types and interfaces of a type. /// </summary> /// <param name="type"></param> /// <returns></returns> private static IEnumerable<Type> GetBaseTypes(Type type) { if (type.BaseType == null) return type.GetInterfaces(); return Enumerable.Repeat(type.BaseType, 1) .Concat(type.GetInterfaces()) .Concat(type.GetInterfaces().SelectMany<Type, Type>(GetBaseTypes)) .Concat(GetBaseTypes(type.BaseType)); } |
Since I’m quite new to using Glass Mapper, I’d be interested to get some feedback on this. I’m pretty sure there’s more that can be tested.
Bye!