Quantcast
Channel: Knifecore
Viewing all articles
Browse latest Browse all 21

Validating your code references to serialized data

$
0
0

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!


Viewing all articles
Browse latest Browse all 21

Trending Articles