Automated testing for Tridion templates: Technical

This is the second post on automated testing for Tridion component templates. The previous post was an overview about the usefulness of a testing strategy and the different approaches to automated testing in general.

This post will focus on actually implementing integration testing for Tridion templates. I will cover integration testing since this approach is well suited for testing template building blocks, which are basically data transformations.

Before we dive into the technical bit, let’s take a moment to discuss the creating the test input for integration tests. Integration tests require data, which has to coded by hand or pulled from somewhere. Both are valid choices, but it is a choice between stability and maintainability:

  • When favoring stability the input has to be local and coded hand in order to avoid a dependency to any external system. When using a hand coded input, changing the tests to accommodate new requirements will take longer since the input will likely have to change as well.
  • When favoring the ability to quickly change tests the input can be pulled from an external component. Since the input isn’t code by hand, it takes less time to change the automated tests. The downside is that you create a dependency on the external system. Downtime of that system causes tests to fail.

Now that we got the options for test input covered, let’s dive into the technical bits. I have two possible implementations for integration testing lined up:

Integration testing using TOM classes with Microsoft Fakes

The first implementation is the creation of an integration test input using a local test setup with the normal TOM (Tridion object model) classes. Since these classes are not developed to be testable I had to use the Microsoft Fakes framework in order to make this work. This framework enables the testing of classes that are not designed to be testable. Using Fakes I was able to create the item fields for a component. See the example code below:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml;

using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Tridion.ContentManager;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.ContentManagement.Fields.Fakes;
using Tridion.ContentManager.Fakes;

namespace Tridion.Deloitte.Libraries.Tests.ContentArticle
{
    [TestClass]
    //Use the default visual studio debugger: http://blog.degree.no/2012/09/visual-studio-2012-fakes-shimnotsupportedexception-when-debugging-tests/
    public class ItemFieldsTests
    {
        [TestMethod]
        public void ItemFieldTest()
        {
            using (ShimsContext.Create())
            {
                //Session constructor is bypassed completely to prevent interaction with Tridion. 
                ShimSession.Constructor = x => { };
                Session session = (Session)Activator.CreateInstance(typeof(Session), new object[] { });

                //Finalize is supressed to prevented interaction with Tridion. 
                GC.SuppressFinalize(session);

                //IdentifiableObject constructor is bypassed completely to prevent interaction with Tridion. 
                ShimIdentifiableObject.ConstructorTcmUriSession = (x, uri, sess) => { };
                Schema schema = (Schema)Activator.CreateInstance(typeof(Schema), new object[] { TcmUri.UriNull, session });

                //Create item fields without any interaction with Tridion using Fakes.
                ItemFields fields = GetItemFields(schema, session);

                //Assert fields where actually created.
                Assert.AreEqual(fields.Count, 1);
            }
        }

        private ItemFields GetItemFields(Schema schema, Session session)
        {
            List fields = new List { GetItemField(session) };

            ShimItemFields.ConstructorSchema = (x, y) =>
            {
                FieldInfo fieldsField = typeof(ItemFields).GetField("_fields", BindingFlags.NonPublic | BindingFlags.Instance);
                if (fieldsField != null)
                {
                    fieldsField.SetValue(x, fields);
                }
            };

            ItemFields itemFields = (ItemFields)Activator.CreateInstance(typeof(ItemFields), new object[] { schema });

            var shimFields = new ShimItemFields(itemFields);
            shimFields.ItemGetString = x => GetItemField(session);

            return itemFields;
        }

        private ItemField GetItemField(Session session)
        {
            ShimItemField.ConstructorXmlElementSession = (x, y, z) => { };

            const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance;
            XmlDocument emptyDoc = new XmlDocument();
            var arguments = new object[] { emptyDoc.DocumentElement, session };

            Type type = typeof(SingleLineTextField);
            var itemField = (ItemField)Activator.CreateInstance(type, bindingFlags, null, arguments, null);

            var shimField = new ShimItemField(itemField);
            shimField.NameGet = () => "Title";

            var field = itemField as SingleLineTextField;
            field.Value = "This is a title field";

            return itemField;
        }
    }
}

The basic implementation above is pretty decent, but you have to code the item fields by hand instead of just using a local xml file with component xml generated by Tridion. As discusses earlier, coding the input by hand effects the time it takes to change the tests.

In order to overcome this I intended to extended the code above to generate item fields from component and schema xml files generated by Tridion. Again, I used Microsoft Fakes to change the behavior of the TOM classes in order to make this scenario testable.

The end result worked fine, but it’s not something I would recommend other developers to try. Microsoft Fakes is a very powerful framework, but relying on it too much has drawbacks in terms of readability. A great deal of Microsoft Fakes configuration was required to make the scenario testable, making the code complex, hard to read and hard to understand. On top of that coding the test and configuring Fakes properly required extensive interaction with the inner workings of Tridion.

In conclusion, taking this route for automated testing has to following advantages and limitations:

Advantages: This implementation makes the TOM classes testable, so the template building blocks themselves do not have to be changed in order to start creating automated tests.

Limitations: This implementation requires expensive versions of Visual Studio and additional tooling for decompiling and debugging Tridion libraries. Coding the test setup requires a lot of interaction with the inner workings of Tridion. A great deal of Microsoft Fakes configuration makes the code complex, hard to read, and hard to understand.

Test input using Tridion Core Service classes

The first approach is the creation of an integration test input using a local test setup with Tridion core service classes. This might not seem like the most obvious approach at first, but is does offer certain advantages. The core service classes are perfect for creating test input since they are simple data classes that be created without any additional effort. Converting the data into fields is also very easy since there is a library by Frank van Puffelen that handles that nicely. See the example code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tridion.ContentManager;
using Tridion.ContentManager.CoreService.Client;

namespace Tridion.Deloitte.Libraries.Tests
{
    [TestClass]
    public class ComponentFieldsTests
    {
        [TestMethod]
        public void TestComponentFieldCreation()
        {
            const string fieldName = "Title";
            const string expectedTitle = "Test";

            ComponentData componentData = new ComponentData { Id = TcmUri.UriNull};
            componentData.Content = string.Format("{0}", expectedTitle);

            ItemFieldDefinitionData definition = new SingleLineTextFieldDefinitionData();
            definition.Name = fieldName;

            SchemaFieldsData schemaFieldsData = new SchemaFieldsData();
            schemaFieldsData.NamespaceUri = "Tridion.Deloitte.Libraries.Tests";
            schemaFieldsData.RootElementName = "Content";
            schemaFieldsData.Fields = new [] { definition };

            ComponentFields contentFields = ComponentFields.ForContentOf(schemaFieldsData, componentData);

            Assert.AreEqual(expectedTitle, contentFields[fieldName].Value);
        }
    }
}

The code above is really simple, but you have to code the fields instead of just using content from Tridion. However, this approach offers the freedom to either code the input yourself or pull it from the core service, since its core service classes you’re using in the first place. See the example code below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tridion.ContentManager;
using Tridion.ContentManager.CoreService.Client;

namespace Tridion.Deloitte.Libraries.Tests
{
    [TestClass]
    public class ComponentFieldsTests
    {
        [TestMethod]
        public void TestComponentFieldCreationWithCoreService()
        {
            const string fieldName = "Title";
            const string expectedTitle = "Test";

            ISessionAwareCoreService client = TridionCoreServiceProvider.InitClient();
            var schemaFieldsData = client.ReadSchemaFields("tcm:106-269739-8", true, new ReadOptions());
            var componentData = (ComponentData)client.Read("tcm:106-273325", new ReadOptions());

            ComponentFields contentFields = ComponentFields.ForContentOf(schemaFieldsData, componentData);
            Assert.AreEqual(expectedTitle, contentFields[fieldName].Value);
        }
    }
}

The code above is also really simple, and saves the time of coding the input yourself. This will reduce the time required to change to tests to accommodate new requirements. There is a risk when you create a dependency on the external system. Downtime of that system causes tests to fail. But since this external system is actually another Tridion component the risks are limited.

From a testing standpoint, using the core service classes is a much better approach. However, this approach might not be favorable for existing implementations with their set of existing template building blocks, which will have to be changed in order to accommodate automated testing using this approach. I would not recommend changing a large set of existing template building blocks, the costs will most likely be higher than the benefits that automated testing will bring.

For new implementations, using the core service classes is a valid choice if automated testing is a requirement. The Tridion object model classes are easily mapped to the core service classes since their basically the same. In the code package I will include classes that map fields from a component to testable fields. This download should offer a decent start.

In conclusion, taking this route for automated testing has to following advantages and limitations:

Advantages: The test setup is simple and doesn’t require any trickery to setup. Data can be easily pulled from the core service, this will reduce the time required to change to tests to accommodate new requirements. Overall the best approach from a testing stand point.

Limitations: In order to use this approach existing templates have to be changed, which is a serious limitation for existing implementations with their set of existing template building blocks.

Wrapping up

Tridion should definitely be looking at automated testing for future version of their product. The Tridion object model classes are a nightmare to work with from a testing perspective. I touched on several possible solutions, but they all have their limitations. There is a lot of room for improvement.

Tridion caters to enterprise clients, the client expect a certain quality from their developers and consultants. We all have to work together in order to deliver that quality. A documented and supported approach to automated testing would benefit the overall quality greatly.

As I noted earlier in my first post, the technical solutions I described are not the only way to achieve automated testing. The stack overflow thread I linked in the beginning of my first post mentions several others, but not in much detail. If there is a good approach I didn’t cover yet please say so in the comments. I will definitely look into it.

2 thoughts on “Automated testing for Tridion templates: Technical

  1. The library at http://code.google.com/p/tridion-practice/wiki/ChangeContentOrMetadata was not written by me. It was written by Frank van Puffelen. Google Code is a great hosting platform for such things, but unfortunately it gives quite some prominence to displaying the name of the last person to edit an item, no matter how small their contribution. If you want to know who wrote something, this information is available, in this case at https://code.google.com/p/tridion-practice/source/list?path=/ChangeContentOrMetadata.wiki&repo=wiki&r=e0d04b509e8128f8120cea7ec232b46446b787de

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>