Sunday, February 4, 2018

Unit testing, Mocks and Dependency Injection in Sitecore 9

In this post I am going to cover how to inject your own dependencies into Sitecore 9's default implementation (Microsoft.Extensions.DependencyInjection abstractions from ASP.NET Core). On top of this I am going to show how these dame dependencies can be unit tested with Mocks.

The technologies which will be used are:
  • NUnit - Unit testing framework
    • In addition the NUnit 3 Test Adapter for Visual Studio
  • Moq - Mocking framework for .NET
  • Sitecore 9 (update-1)

Setting up the logic to inject

The first step is to create an interface and implementer which will be what is injected into Sitecore. In a real world example this may be an external service layer or perhaps a data access layer. In this case it's a simple example of an account interface.

First up is the model which is the return type for the method.
namespace MyProject.Foundation.ServiceLayer.Account.Model
{
    public class UserModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
    }
}
This is the implementation of the IAccountService interface.
namespace MyProject.Foundation.ServiceLayer.Account
{
    public class AccountService : IAccountService
    {
        public UserModel GetUser(string emailAddress)
        {
            // logic here

            return new UserModel
            {
                EmailAddress = "Ryan.Bailey@email.com",
                FirstName = "Ryan",
                LastName = "Bailey"
            };
        }
    }
}
This is the IAccountService interface.
namespace MyProject.Foundation.ServiceLayer.Account
{
    public interface IAccountService
    {
        UserModel GetUser(string emailAddress);
    }
}
It's a simple example, but normally the GetUser implementation would do a lot more (such as call a service or read from a database). I also prefer to use models rather than parameters direct (such as email address in this case).

Unit testing with mocks

Now we have an interface (and implementer) with a single method, it's a good idea to create a unit test. Of course the unit test shouldn't actually be calling a service or reading from database, so we need to use the concept of Mocks. Mocks allow us to test and focus on the code being tested, rather than worrying about external dependencies (which integration testing should cover).

A new Unit Test Project should be created in your Visual Studio solution, the following is an example of a test for the GetUser method. 
using System;
using NUnit.Framework;
using MyProject.Foundation.ServiceLayer.Account;
using Moq;
using MyProject.Foundation.ServiceLayer.Account.Model;

namespace MyProject.Foundation.ServiceLayer.Tests.Account
{
    [TestFixture]
    public class AccountUnitTest
    {
        private Mock<IAccountService> mockAccountService;

        [OneTimeSetUp]
        public void SetupTests()
        {
            mockAccountService = new Mock<IAccountService>();

            // GetUser
            var user = new UserModel
            {
                EmailAddress = "Ryan.Bailey@email.com",
                FirstName = "Ryan",
                LastName = "Bailey"
            };

            mockAccountService.Setup(m => m.GetUser(It.IsAny<string>())).Returns(user);
        }

        [Test]
        public void TestGetUser()
        {
            var user = mockAccountService.Object.GetUser("test@email.com");

            Assert.AreEqual("Ryan", user.FirstName);
            Assert.AreEqual("Bailey", user.LastName);
        }
    }
}
In this example our TestFixture makes use of the OneTimeSetUp attribute to perform setup actions before running through the Tests. In here we Mock the IAccountService interface and it's GetUser method. In this example, any request for GetUser (note the IsAny on the email address string parameter), will return the mocked data.

Further down in the actual test, the GetUser method is called on the mock service and we are asserting that the data matches what was expected - this unit test will pass.

Test Explorer in Visual Studio

This again is a basic example, for a unit test on a login method, you may mock out a successful and failed login and then have a unit test for each. The idea is to test all aspects of the model/code to ensure complete unit test coverage.

Hooking into Sitecore 9 Dependency Injection

Now that we have our IAccountService interface, it would be nice to inject this and access it in our MVC controllers. We are going to register our dependency in code by implementing Sitecore's IServicesConfigurator.
using MyProject.Foundation.ServiceLayer.Account;
using Sitecore.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

namespace MyProject.Foundation.BackEnd.Common.DependencyInjection
{
    public class MyServicesConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddScoped(typeof(IAccountService), typeof(AccountService));

            serviceCollection.AddMvcControllers("MyProject.Feature.*");
        }
    }
}
The AddScoped call comes with Sitecore and allows us to register our dependency quite simply. The AddMvcControllers call is a custom configurator from Kam Figy, which allows us to easily register the MVC Controllers we wish to dependency inject into. As I am following Sitecore Helix I can register all of my features in one simple line.

The custom configurator is below:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace MyProject.Foundation.BackEnd.Common.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        [MethodImplAttribute(MethodImplOptions.NoInlining)]
        public static void AddMvcControllersInCurrentAssembly(this IServiceCollection serviceCollection)
        {
            AddMvcControllers(serviceCollection, Assembly.GetCallingAssembly());
        }

        public static void AddMvcControllers(this IServiceCollection serviceCollection, params string[] assemblyFilters)
        {
            var assemblyNames = new HashSet<string>(assemblyFilters.Where(filter => !filter.Contains('*')));
            var wildcardNames = assemblyFilters.Where(filter => filter.Contains('*')).ToArray();

            var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(assembly =>
            {
                var nameToMatch = assembly.GetName().Name;
                if (assemblyNames.Contains(nameToMatch)) return true;

                return wildcardNames.Any(wildcard => IsWildcardMatch(nameToMatch, wildcard));
            })
            .ToArray();

            AddMvcControllers(serviceCollection, assemblies);
        }

        public static void AddMvcControllers(this IServiceCollection serviceCollection, params Assembly[] assemblies)
        {
            var controllers = GetTypesImplementing<IController>(assemblies)
                .Where(controller => controller.Name.EndsWith("Controller", StringComparison.Ordinal));

            foreach (var controller in controllers)
            {
                serviceCollection.AddTransient(controller);
            }

            // h/t Sean Holmesby and Akshay Sura: this adds Web API controller support
            //var apiControllers = GetTypesImplementing<ApiController>(assemblies)
               // .Where(controller => controller.Name.EndsWith("Controller", StringComparison.Ordinal));

            //foreach (var apiController in apiControllers)
            //{
                //serviceCollection.AddTransient(apiController);
            //}
        }

        public static Type[] GetTypesImplementing<T>(params Assembly[] assemblies)
        {
            if (assemblies == null || assemblies.Length == 0)
            {
                return new Type[0];
            }

            var targetType = typeof(T);

            return assemblies
                .Where(assembly => !assembly.IsDynamic)
                .SelectMany(GetExportedTypes)
                .Where(type => !type.IsAbstract && !type.IsGenericTypeDefinition && targetType.IsAssignableFrom(type))
                .ToArray();
        }

        private static IEnumerable<Type> GetExportedTypes(Assembly assembly)
        {
            try
            {
                return assembly.GetExportedTypes();
            }
            catch (NotSupportedException)
            {
                // A type load exception would typically happen on an Anonymously Hosted DynamicMethods
                // Assembly and it would be safe to skip this exception.
                return Type.EmptyTypes;
            }
            catch (ReflectionTypeLoadException ex)
            {
                // Return the types that could be loaded. Types can contain null values.
                return ex.Types.Where(type => type != null);
            }
            catch (Exception ex)
            {
                // Throw a more descriptive message containing the name of the assembly.
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
                    "Unable to load types from assembly {0}. {1}", assembly.FullName, ex.Message), ex);
            }
        }

        /// <summary>
        /// Checks if a string matches a wildcard argument (using regex)
        /// </summary>
        private static bool IsWildcardMatch(string input, string wildcards)
        {
            return Regex.IsMatch(input, "^" + Regex.Escape(wildcards).Replace("\\*", ".*").Replace("\\?", ".") + "$", RegexOptions.IgnoreCase);
        }
    }
}
Of course we will need to register the MyServicesConfigurator code with Sitecore (via configuration) to ensure it is injected correctly.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <services>
            <configurator type= "MyProject.Foundation.BackEnd.Common.DependencyInjection.MyServicesConfigurator, MyProject.Foundation.BackEnd.Common"/>
        </services>
    </sitecore>
</configuration>

Calling the injected interface

Now that we have injected our interface into the Sitecore DI provider, it's time to access it via an MVC controller. This is quite simply done as illustrated below:
using System.Web.Mvc;
using MyProject.Feature.Common.Models;
using MyProject.Foundation.ServiceLayer.Account;

namespace MyProject.Feature.Common.Controllers
{
    public class CommonFeatureController : Controller
    {
        private readonly IAccountService _accountService;

        public CommonFeatureController(IAccountService accountService)
        {
            _accountService = accountService;
        }

        public ActionResult Test()
        {
            var user = _accountService.GetUser("test@email.com");

            var model = new TestModel
            {
                EmailAddress = user.EmailAddress
            };

            return this.View(model);
        }
    }
}
In this example, I have a CommonFeatureController with a Test method which returns a model to the Test view. It's simply trying to show how the AccountService can be accessed and called and data then passed through to the view.

No comments:

Post a Comment