Allowing unit tests using generic funcs, actions or expressions

The other day, I was required during writing a unit test on a Sitecore pipeline to be able to get the value from a method in a base class in the original Sitecore API. In this case it was Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, and what I needed to do was get the cachekey from the base and then modify it to add additional string values to the key. In doing so I realised that the standard ‘abstract it away’ didn’t really work in its traditional form, since the abstraction would not be aware of ‘base.’. I could pass in that specific method as an argument, but felt that a better approach might be to create a utility repository that would allow me to execute code with an expected return. This meant using a normal Mock of this approach, I could abstract the code in question and simply return what I expect to be the value, without worrying about it (in this case) actually getting the API involved.

The problem

This was similar to the code I was working on where AnonymousRepository was an injected constructor parameter for IAnonymousRepository:

        protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
        {
            string cacheKey = AnonymousRepository.GetResult(() => base.GenerateKey(rendering, args));
            // modify the cache key from here
        }

In my test code I could therefore gain control of the return and pass back a value, and also assert that it was called like this:

            // Assign
            Mock<IAnonymousRepository> anonMock = new Mock<IAnonymousRepository>();
            anonMock.Setup(x => x.GetResult(It.IsAny<Func<string>>())).Returns(originalCacheKey);

            // Act - do something

            // Assert
            anonMock.Verify(x => x.GetResult(It.IsAny<Func<string>>()), Times.Once);

It is worth noting at this point that I am using the Moq – It.IsAny. This is due to the fact that I am using a expressions as the lambda. If I created a method I could get to (like using an internal method and exposing it to the unit test assembly via [InternalsVisibleTo(“testassembly”)] then I *believe* I would be able to do a more concrete assertion on the method being called since the call to AnonymousRepository.GetResult(MyMethodName); – in this instance I believe anonMock.Verify(x => x.GetResult(classUnderTest.MyMethodName), Times.Once); would be ok. I am not 100% certain of this though. In this case, there is only 1 abstracted method, so I am ok with it.

The anonymous repository

Here is how I wrote the anonymous repository


    /// <summary>
    /// An anonymous helper that allows results to be drawn from code which is unknown.
    /// </summary>
    public interface IAnonymousRepository
    {
        /// <summary>
        /// Gets the result of a function (usually that cannot be tested successfully)
        /// </summary>
        /// <returns></returns>
        T GetResult<T>(Func<T> resultFunc);
    }

    public class AnonymousRepository : IAnonymousRepository
    {
        public T GetResult<T>(Func<T> resultFunc)
        {
            if (resultFunc == null)
            {
                throw new ArgumentNullException("resultFunc", "Result function cannot be null");
            }

            return resultFunc();
        }
    }

Conclusion

Whilst this approach utilises a Func, you could equally use Action, Expression etc to achieve similar.

It is worth noting however, I consider this a last resort, and would favour a more solid abstraction over this method due to the relatively transient nature in which the expressions are asserted against.

Hope this helps

Nat

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s