Meaningful TDD and unit tests with Sitecore and DI

This is a less Sitecore-oriented post than usual, although very much relevant in a Sitecore application context. After a couple of years of getting frustrated when trying to apply TDD principles and to try and leverage some benefit from unit tests, I was inspired by this video (and associated content such as this, this and this) to try a different tack, in an attempt to try and get my unit tests to work harder for me.

Do you write a unit test for every class? Or if not every class, is each test focussed at class level, mocking any dependencies with other classes? Do you feel that your tests express any meaningful behaviour which relates to the feature you are working on, or do the names of the tests simply reflect the connections between one class and its collaborating classes? Are you finding that your tests are highly coupled to your implementation, such that whenever you make a refactoring change, all of your tests break, discouraging you from performing cleansing of your code? And moreover, does this approach put the brakes on any hope of Test Driven Development, because you have to have a clear idea of how you are going to set up your implementation as classes, before being able to write any tests?

The tl;dr of the alternative approach is - why not test per unit of functionality rather than per class? I.e. test at the boundaries, or ports of your application (for Sitecore developers, this would tend to be at the entry point to an MVC controller, pipeline processor etc). This doesn't necessarily make it a full stack integration test, as you would still mock truly external dependecies - references to the Sitecore API, config files, the filesystem etc. But any classes which you have defined yourself can be deemed as "implementation" or "internals", and generally not tested directly - instead these are tested implicitly via the "public interface" of the functionality under test. You should start to gain the ability to refactor implementation without breaking tests (i.e. it won't matter how many layers of “implementation” you have between the port boundary and your mocked dependencies) and tests should appear more meaningful (coupled to features rather than classes). In addition, you can start doing real TDD development, as you won't need to think about your implementation in terms of classes - just in terms of the effect on the "public interface".

This style of testing brings with it its own set of downsides and challenges, and I've not seen very much evidence of its usage both in my work or documented online - but my limited experience of trying it out in the field has show lots of positive signs so far. Refactoring with a decent set of tests to back you up (which won't break as you refactor) is a great experience; after getting an initial crude implementation of a feature, you can switch your brain into refactoring mode and purely focus on making your code as clean a possible - splitting the implementation up exactly as fits. Suddenly the concepts in Beck's book start to feel a lot more practical! When I started to try out this approach, I wanted to see how I could exploit a DI setup to make writing feature-level tests even easier, but I couldn't find many practical examples - so the approach I'll go over now is very much a work-in-progress/experiment, please let me know if you have any ideas or feedback!

Consider the example of a RenderField pipeline processor, which looks for specific "magical" text tokens in certain field types, and replaces them with a value from a dictionary (one way - albeit fairly intrusive - of enabling content editors to spread certain content pervasively across an application). The processor class itself is dumb and simply dispatches control to a "helper" class which uses abstractions to make calls to Sitecore API objects - making the logic fully testable:

        namespace RedMoon.Example.Pipelines.RenderField
        {
            public class TokenReplacer
            {
                private readonly ITokenReplacerHelper _tokenReplacerHelper = // Get instance of ITokenReplacerHelper from your DI container here
 
                public void Process(RenderFieldArgs args)
                {
                    _tokenReplacerHelper.ReplaceTokens(new RenderFieldArgsWrapper(args));
                }           
            }
        }
    

The helper class code is shown below - note that only the relevant (text-based) field types are eligible to have the token-replacement behaviour applied, and that in page editor mode the logic is also not run. The tokens are key/value pairs defined as children of a known Sitecore item:

        namespace RedMoon.Example.Pipelines.RenderField
        {
            public class TokenReplacerHelper : ITokenReplacerHelper
            {
                private const string TokenReplacementMessage = "Token Replacements took {0} ms";
                private readonly string[] _replaceableFieldTypes = { "rich text", "multi-line text", "single-line text" };

                private readonly ISitecoreService _sitecoreService;  
                private readonly ISiteContext _siteContext;
                private readonly ICache _cache;

                public TokenReplacerHelper(ISitecoreService sitecoreService,
                    ISiteContext siteContext,
                    ICache cache)
                {
                    _sitecoreService = sitecoreService;      
                    _siteContext = siteContext;
                    _cache = cache;
                }

                public void ReplaceTokens(IRenderFieldArgs args)
                {           
                    if (!ShouldReplaceText(args.FieldTypeKey))
                    {
                        return;
                    }

                    var firstPart = args.Result.FirstPart;
                    if (string.IsNullOrEmpty(firstPart))
                    {
                        return;
                    }

                    var tokensToReplace = _cache.Retrieve<dictionary<string, string>>(CacheKeys.TokensKey);
                    if (tokensToReplace == null)
                    {
                        tokensToReplace = GetTokens();
                        _cache.AddObject(CacheKeys.TokensKey, tokensToReplace);
                    }

                    if (tokensToReplace.Any())
                    {
                        var timer = Stopwatch.StartNew();

                        args.Result.FirstPart = ReplaceAllTokens(tokensToReplace, firstPart);

                        timer.Stop();
                        Log.Debug(string.Format(TokenReplacementMessage, timer.Elapsed.TotalMilliseconds), this);
                    }
                }

                private bool ShouldReplaceText(string fieldType)
                {
                    return (!_siteContext.PageModeIsPageEditor && _replaceableFieldTypes.Contains(fieldType));
                }

                private Dictionary<string, string> GetTokens()
                {
                    var tokenReplacementsFolderItemId = new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}"); // (Should be defined in an "ItemIds" class somewhere else)
                    var tokenItems = _sitecoreService.GetChildren(tokenReplacementsFolderItemId);

                    var tokensToReplace = new Dictionary<string, string>();
                    foreach (var token in tokenItems)
                    {
                        tokensToReplace[_sitecoreService.RawFieldValue(token, "Key")] = _sitecoreService.RawFieldValue(token, "Text");
                    }

                    return tokensToReplace;
                }

                private string ReplaceAllTokens(Dictionary<string, string> tokensToReplace, string initialFieldValue)
                {
                    return tokensToReplace.Aggregate(initialFieldValue, (current, token) => current.Replace(token.Key, token.Value, StringComparison.CurrentCultureIgnoreCase));
                }
            }
        }
    

Now let's have a look at the unit tests for the helper class:

        [TestFixture]
        namespace RedMoon.Example.Tests.Pipelines.RenderField
        {
            public class TokenReplacerHelperTests
            {
                private IContainer _diContainer;
                private readonly Guid _tokenItem1 = new Guid("{D9C93F0A-90E2-4202-801F-31098A629F64}");
                private readonly Guid _tokenItem2 = new Guid("{8054BFFA-86A5-4DC3-89EE-E7C1AEB5F485}");
                private Mock<ISitecoreService> _sitecoreService;
                private Mock<ISiteContext> _siteContext;
                private Mock<ICache> _cache;
                private Mock<IRenderFieldResult> _renderFieldResult;
                private Mock<IRenderFieldArgs> _renderFieldArgs;

                [SetUp]
                public void SetUp()
                {
                    SetUpValidTokenSubstitutionConditions();
                }

                [Test]
                public void WhenReplaceTokensIsCalledThenArgsResultIsSetCorrectly()
                {
                    SetUpExternalDependencies();
ReplaceTokens();
_renderFieldResult.VerifySet(r => r.FirstPart = "Something token text 1 something else token text 2 token text 2"); } [Test] public void WhenReplaceTokensIsCalledAndCacheHasTokensThenArgsResultIsSetCorrectly() { var dictionary = new Dictionary<string, string> { {"$TokenKey1$", "token text 1"}, {"$TokenKey2$", "token text 2"} }; _sitecoreService = new Mock<ISitecoreService>(); _cache.Setup(c => c.Retrieve<dictionary<string, string>>("Tokens")) .Returns(dictionary); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = "Something token text 1 something else token text 2 token text 2"); } [Test] public void WhenTokensIsCalledAndFieldTypeIsNotReplaceableThenArgsResultIsNotSet() { _renderFieldArgs.Setup(a => a.FieldTypeKey).Returns("Image"); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never); } [Test] public void WhenTokensIsCalledAndPageModeIsEditThenArgsResultIsNotSet() { _siteContext.Setup(c => c.PageModeIsPageEditor).Returns(true); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never); } [Test] public void WhenTokensIsCalledAndFieldValueIsEmptyThenArgsResultIsNotSet() { _renderFieldArgs.Setup(a => a.Result.FirstPart).Returns(string.Empty); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never); } [Test] public void WhenTokensIsCalledAndFieldValueIsNullThenArgsResultIsNotSet() { _renderFieldArgs.Setup(a => a.Result.FirstPart).Returns(null as string); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never); } [Test] public void WhenTokensIsCalledAndNoTokenItemsInSitecoreThenArgsResultIsNotSet() { _sitecoreService.Setup(s => s.GetChildren(new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}"))).Returns(new Guid[0]); SetUpExternalDependencies(); ReplaceTokens(); _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never); } private void SetUpValidTokenSubstitutionConditions() { SetUpDependencies(); SetUpArgs(); } private void SetUpExternalDependencies() { var builder = new ContainerBuilder(); MockExternalDependencies(builder); _diContainer = builder.Build(); } private void SetUpDependencies() { _sitecoreService = new Mock<ISitecoreService>(); _sitecoreService.Setup(s => s.GetChildren(new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}"))) .Returns(new[] { _tokenItem1, _tokenItem2 }); _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem1, "Key")).Returns("$TokenKey1$"); _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem1, "Text")).Returns("token text 1"); _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem2, "Key")).Returns("$TokenKey2$"); _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem2, "Text")).Returns("token text 2"); _siteContext = new Mock<ISiteContext>(); _siteContext.Setup(c => c.PageModeIsPageEditor).Returns(false); _cache = new Mock<ICache>(); _cache.Setup(c => c.Retrieve<Dictionary<string, string>>("Tokens")) .Returns(null as Dictionary<string, string>); } private void SetUpArgs() { _renderFieldResult = new Mock<IRenderFieldResult>(); _renderFieldResult.Setup(a => a.FirstPart).Returns("Something $TokenKey1$ something else $TokenKey2$ $TokenKEY2$"); _renderFieldArgs = new Mock<IRenderFieldArgs>(); _renderFieldArgs.Setup(a => a.FieldTypeKey).Returns("single-line text"); _renderFieldArgs.Setup(a => a.Result).Returns(_renderFieldResult.Object); } private void MockExternalDependencies(ContainerBuilder builder) { // Wire up dependencies as used by your application - in practice you'd separate these off into separate config classes builder.RegisterType<TokenReplacerHelper>().As<ITokenReplacerHelper>().SingleInstance(); builder.RegisterType<SitecoreService>().As<ISitecoreService>().SingleInstance(); builder.RegisterType<SiteContext>().As<ISiteContext>().SingleInstance(); builder.RegisterType<Cache>().As<ICache>().SingleInstance(); // ... many more dependencies here // Here we are permitted to override concrete class implementation with mocks - but only if it's truly // external (Sitecore API, configuration, filesystem, DB access etc) OverrideDefaultExternalDependencies(builder); } private void OverrideDefaultExternalDependencies(ContainerBuilder builder) { builder.RegisterInstance(_sitecoreService.Object).SingleInstance(); builder.RegisterInstance(_siteContext.Object).SingleInstance(); builder.RegisterInstance(_cache.Object).SingleInstance(); } private void ReplaceTokens() { var tokenReplacerHelper = _diContainer.Resolve<ITokenReplacerHelper>(); tokenReplacerHelper.ReplaceTokens(_renderFieldArgs.Object); } } }

In an ideal world we would try and limit ourselves to only checking the return value of the method being tested - introducing "verify" calls starts coupling our test code to the implementation code again ... something which we're trying to avoid. However, with our Sitecore abstractions we've tried to mimic the way the wrapped code works - hence, the helper class "result" is to set properties on the arguments passed in - hopefully this compromise helps users of the abstracted code implement in a familiar way (and keeps the feel of the "pipelines" concept.) You can also see that we reuse the same DI registrations that the application uses - this is a neat way to wire together the necessary dependencies for the system-under-test (no need to construct and pass in all the dependencies ourselves), although we are creating a new container for each test. We set mocks for the truly external dependencies - in this case, calls to the database via Sitecore and calls to Sitecore's context objects. In-memory caching is also mocked. Actually, there are no dependencies of the system-under-test which remain unmocked - however, this now changes as we start to think about refactoring the helper class...

Now that we have working code which makes our tests pass, and we feel that we've covered all scenarios which are required by the functionality, we can turn our attention to making the code cleaner and more maintainable. The helper class is quite large, and it looks like some of the code relating to caching could be turning up in various places in our codebase - so let's extract an "implementation" class to encapsulate our cache-checking behaviour:

        namespace RedMoon.Example.Caching
        {
            public class CacheService : ICacheService
            {
                private readonly ICache _cache;

                public CacheService(ICache cache)
                {
                    _cache = cache;
                }

                public T EnsureCache<t>(string cacheKey, Func<t> method) where T : class
                {
                    var content = _cache.Retrieve<t>(cacheKey);
                    if (content != null)
                    {
                        return content;
                    }

                    content = method();

                    if (content != null)
                    {
                        _cache.AddObject(cacheKey, content);
                    }

                    return content;
                }
            }
        }
    

Now the helper class looks a bit more focussed on its responsibilities:

        namespace RedMoon.Example.Pipelines.RenderField
        {
            public class TokenReplacerHelper : ITokenReplacerHelper
            {
                private const string TokenReplacementMessage = "Token Replacements took {0} ms";
                private readonly string[] _replaceableFieldTypes = { "rich text", "multi-line text", "single-line text" };

                private readonly ISitecoreService _sitecoreService;  
                private readonly ISiteContext _siteContext;
                private readonly ICacheService _cacheService;

                public TokenReplacerHelper(ISitecoreService sitecoreService,
                    ISiteContext siteContext,
                    ICacheService cacheService)
                {
                    _sitecoreService = sitecoreService;      
                    _siteContext = siteContext;
                    _cacheService = cacheService;         
                }

                public void ReplaceTokens(IRenderFieldArgs args)
                {           
                    if (!ShouldReplaceText(args.FieldTypeKey))
                    {
                        return;
                    }

                    var firstPart = args.Result.FirstPart;
                    if (string.IsNullOrEmpty(firstPart))
                    {
                        return;
                    }

                    var tokensToReplace = _cacheService.EnsureCache(CacheKeys.TokensKey, GetTokens);                        
                    if (tokensToReplace.Any())
                    {
                        var timer = Stopwatch.StartNew();
                        args.Result.FirstPart = ReplaceAllTokens(tokensToReplace, firstPart);
                        timer.Stop();
                        Log.Debug(string.Format(TokenReplacementMessage, timer.Elapsed.TotalMilliseconds), this);
                    }
                }

                private bool ShouldReplaceText(string fieldType)
                {
                    return (!_siteContext.PageModeIsPageEditor && _replaceableFieldTypes.Contains(fieldType));
                }

                private Dictionary<string, string> GetTokens()
                {           
                    var tokenReplacementsFolderItemId = new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}"); // (Should be defined in an "ItemIds" class somewhere else)
                    var tokenItems = _sitecoreService.GetChildren(tokenReplacementsFolderItemId);

                    var tokensToReplace = new Dictionary<string, string>();
                    foreach (var token in tokenItems)
                    {
                        tokensToReplace[_sitecoreService.RawFieldValue(token, "Key")] = _sitecoreService.RawFieldValue(token, "Text");
                    }

                    return tokensToReplace;
                }

                private string ReplaceAllTokens(Dictionary<string, string> tokensToReplace, string initialFieldValue)
                {
                    return tokensToReplace.Aggregate(initialFieldValue, (current, token) => current.Replace(token.Key, token.Value, StringComparison.CurrentCultureIgnoreCase));
                }
            }
        }
    

When we run our tests, we find that they pass without any modification; If you're trying out the example code, you'll have to add the ICacheService registration into the example test code, but we're assuming that, in practise, these registrations would be encapsulated elsewhere (to be used by the application) and then simply called by the test code. So the concrete CacheService is used by the tests. You can imagine the benefits of this approach being even greater as the complexity of the system-under-test grows - you can break up your working "implementation" code and verify that it continues to function along the way.

Finally, just a few suggestions to start to hide some of the boilerplate code in the test class above. I've used inheritance to get started, but there may be better ways of doing this - it's very much a work in progress. The next step would probably be to add a "resolve" method, to avoid the need to expose the DI container class to the derived test classes. Here's a "base" class your tests can inherit from:

        namespace RedMoon.Example.Tests.Common
        {
            [TestFixture]
            public class TestBase
            {
                protected IContainer DiContainer;

                protected void SetUpExternalDependencies()
                {
                    var builder = new ContainerBuilder();
                    MockExternalDependencies(builder);

                    DiContainer = builder.Build();
                }

                private void MockExternalDependencies(ContainerBuilder builder)
                {          
                    // Code to register your application dependencies here, by calling
                    // the same code which your application does,           
                    // passing in your "builder"

                    OverrideDefaultExternalDependencies(builder);
                }

                protected virtual void OverrideDefaultExternalDependencies(ContainerBuilder builder)
                {
                }
            }    
        }
    

Now the test class doesn't have to worry about DI as much, save for optionally implementing the OverrideDefaultExternalDependencies method (where we push in instances of the classes which we wish to mock):

namespace RedMoon.Example.Tests.Pipelines.RenderField
{
    public class TokenReplacerHelperTests : TestBase
    {   
        private readonly Guid _tokenItem1 = new Guid("{D9C93F0A-90E2-4202-801F-31098A629F64}");
        private readonly Guid _tokenItem2 = new Guid("{8054BFFA-86A5-4DC3-89EE-E7C1AEB5F485}");

        private Mock<ISitecoreService> _sitecoreService;
        private Mock<ISiteContext> _siteContext;
        private Mock<ICache> _cache;

        private Mock<IRenderFieldResult> _renderFieldResult;
        private Mock<IRenderFieldArgs> _renderFieldArgs;

        [SetUp]
        public void SetUp()
        {
            SetUpValidTokenSubstitutionConditions();
        }

        [Test]
        public void WhenReplaceTokensIsCalledThenArgsResultIsSetCorrectly()
        {
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = "Something token text 1 something else token text 2 token text 2");
        }

        [Test]
        public void WhenReplaceTokensIsCalledAndCacheHasTokensThenArgsResultIsSetCorrectly()
        {
            var dictionary = new Dictionary<string, string>
            {
                {"$TokenKey1$", "token text 1"},
                {"$TokenKey2$", "token text 2"}
            };
            _sitecoreService = new Mock<ISitecoreService>();
            _cache.Setup(c => c.Retrieve<Dictionary<string, string>>("Tokens"))
                .Returns(dictionary);           
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = "Something token text 1 something else token text 2 token text 2");
        }

        [Test]
        public void WhenTokensIsCalledAndFieldTypeIsNotReplaceableThenArgsResultIsNotSet()
        {
             _renderFieldArgs.Setup(a => a.FieldTypeKey).Returns("Image");
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never);
        }

        [Test]
        public void WhenTokensIsCalledAndPageModeIsEditThenArgsResultIsNotSet()
        {
            _siteContext.Setup(c => c.PageModeIsPageEditor).Returns(true);
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never);
        }

        [Test]
        public void WhenTokensIsCalledAndFieldValueIsEmptyThenArgsResultIsNotSet()
        {
            _renderFieldArgs.Setup(a => a.Result.FirstPart).Returns(string.Empty);
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never);
        }

        [Test]
        public void WhenTokensIsCalledAndFieldValueIsNullThenArgsResultIsNotSet()
        {
            _renderFieldArgs.Setup(a => a.Result.FirstPart).Returns(null as string);
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never);
        }

        [Test]
        public void WhenTokensIsCalledAndNoTokenItemsInSitecoreThenArgsResultIsNotSet()
        {
            _sitecoreService.Setup(s => s.GetChildren(new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}"))).Returns(new Guid[0]);
            SetUpExternalDependencies();

            ReplaceTokens();

            _renderFieldResult.VerifySet(r => r.FirstPart = It.IsAny<string>(), Times.Never);
        }

        private void SetUpValidTokenSubstitutionConditions()
        {
            SetUpDependencies();
            SetUpArgs();
        }
      
        private void SetUpDependencies()
        {
            _sitecoreService = new Mock<ISitecoreService>();
            _sitecoreService.Setup(s => s.GetChildren(new Guid("{6139204B-024E-44E1-BD14-61BEE8929B9E}")))
                .Returns(new[] { _tokenItem1, _tokenItem2 });
            _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem1, "Key")).Returns("$TokenKey1$");
            _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem1, "Text")).Returns("token text 1");
            _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem2, "Key")).Returns("$TokenKey2$");
            _sitecoreService.Setup(s => s.RawFieldValue(_tokenItem2, "Text")).Returns("token text 2");

            _siteContext = new Mock<ISiteContext>();
            _siteContext.Setup(c => c.PageModeIsPageEditor).Returns(false);

            _cache = new Mock<ICache>();
            _cache.Setup(c => c.Retrieve<Dictionary<string, string>>("Tokens"))
                .Returns(null as Dictionary<string, string>);
        }

        private void SetUpArgs()
        {
            _renderFieldResult = new Mock<IRenderFieldResult>();
            _renderFieldResult.Setup(a => a.FirstPart).Returns("Something $TokenKey1$ something else $TokenKey2$ $TokenKEY2$");

            _renderFieldArgs = new Mock<IRenderFieldArgs>();
            _renderFieldArgs.Setup(a => a.FieldTypeKey).Returns("single-line text");
            _renderFieldArgs.Setup(a => a.Result).Returns(_renderFieldResult.Object);
        }      

        protected override void OverrideDefaultExternalDependencies(ContainerBuilder builder)
        {
            builder.RegisterInstance(_sitecoreService.Object).SingleInstance();
            builder.RegisterInstance(_siteContext.Object).SingleInstance();
            builder.RegisterInstance(_cache.Object).SingleInstance();
        }

        private void ReplaceTokens()
        {
            var tokenReplacerHelper = DiContainer.Resolve<ITokenReplacerHelper>();
            tokenReplacerHelper.ReplaceTokens(_renderFieldArgs.Object);       
        }       
    }
}

 

 


By James at 13 Dec 2015, 21:22 PM


Comments

Post a comment