Layer-based automated testing, part 2: registration reusable in tests

Previous article contained some general discussion on where the layer-based approach to testing is applicable. Now, let's get into details of implementation. To be able to test layers, you'll need to be able to easily separate them one from another, and easily reuse them as a whole from different entry points (from actual application, from tests, etc).

The basic idea is:

  1. You should use IoC to separate one layer from another
  2. Each layer should contain information about interfaces it implements, and expected lifecycles of these implementations.
  3. In tests, reuse the registration sections for layers. Use the same registrations for layers you intend to test, and mock interfaces from layers you intend to mock.

Note: reusable registration can help a lot not only in building test infrastructure, but in other scenarios. It will prevent copy/paste if you'll have several entry points, and will help you to make the structure of registration more clear. So techniques described in this article can be helpful even if you don't intend to write layer-based testing. However there are different opinions on this topic, see for example this discussion on stackoverflow. Anyway, it will be much harder to do layer-based automated testing without reusable registration.

Application structure

So, let's take as a sample a WPF application. Let's suppose we intend to make our WPF application cross-platform along the recommendations for cross-platform development for .Net 4.5. It means that all WPF specifics is just a thin layer connected with a separate reusable "ViewModel" layer through binding. Also, it's not a good idea to mix a business logic with view models, so we'll create a separate "BusinessLogic" layer which knows nothing about "ViewModel" layer. And of course all data persistence specifics should be extracted into separate "Data" layer, because on different platforms the way we persist the data may change, and anyway it's a good practice not to mix data access specifics with business logic.

Also, let's suppose our application also needs to integrate with some devices, so we'll create also a "Devices" layer where all communication specifics should go. And we need to introduce a common "Infrastructure" project declaring interfaces for communication between layers. In fact, it would be good to introduce several such "Infrastructure"projects, but let's not overcomplicate our sample. The "WPF" project will be an entry point for our application, and we intend to have a separate entry point for each platform, so "Windows Store", "Android" and "Windows Phone" will be other entry points which could appear in the future.

In our layer-based tests we intend to cover the whole "ViewModel" and "BusinessLogic" layers, making them work as close to real application as possible. And we need to mock "WPF", "Devices" and "Data" because they are slow and platform-specific. It does not mean we do not intend to cover them by tests at all. They will be covered, but by different kinds of tests. We intend to cover "Devices" and "Data" by integration tests checking how these layers work on real database and real hardware devices. And we intend to write some graphical UI tests to ensure all is OK with our markup, and to make a set of simple sanity checks for the whole systems with as little mocking as possible. See previous article discussing the choosing of test strategy in more detail.

Registering layers

In fact, there are several ways to define registrations inside layers:

  1. Reference your IoC library in all your layers and write registration section directly inside every layer. Then, at entry point call registration sections from layers one by one. This is a valid approach if by some reasons you already needing to reference IoC library inside your business logic. It may happen if you actively using library-specific interfaces inside your business logic classes: for example, you are using AutoFac and using owned instances to manage lifetime directly. But this is not a clean approach, especially because you are trying to be cross-platform and not sure how particular IoC container will behave on certain platform.
  2. Use assembly scanning. Employ some implicit rules so you won't need a registration section at all. For example, use naming conventions or register all implementations of interfaces you can find by reflection. But maybe this is too implicit and container-specific.
  3. Use custom class attribute to register the supposed lifecycle of this implementation. Not a bad way at all, but maybe too much reflection on the start of the program. And purists may complain that lifecycle should be defined outside the code of the class.
  4. Use custom object wrappers for registration.

I'll demonstrate the fourth way as it's the simplest solution allowing you to stay away from IoC specifics. We'll just need to fill the following structure:

    public struct RegistrationInfo
    {
        public Type Implementation { get; set; }
        public Type Interface { get; set; }
        public object Key { get; set; }
        public Lifecycles Lifecycle { get; set; }
    }

where Lifecycle defines possible lifecycles your object can have:

    public enum Lifecycles
    {
        Singleton,
        PerScope,
        PerDependency
    }

Now generalizing this registration through ModuleRegistrator class, which is exposing layer registrations through Registrations property:

    public abstract class ModuleRegistrator
    {
        protected ModuleRegistrator()
        {
            Registrations = new List<RegistrationInfo>();
        }
 
        public List<RegistrationInfo> Registrations { get; private set; }
 
        public static ModuleRegistrator Create<T>() where T : ModuleRegistrator, new()
        {
            var ret = new T();
            ret.Init();
            return ret;
        }
 
        protected abstract void Init();
 
        protected void Register<Class, Interface>(Lifecycles lifecycle = Lifecycles.PerScope) where Class : Interface
        {
            Registrations.Add(
                new RegistrationInfo
                    {
                        Implementation = typeof(Class),
                        Interface = typeof(Interface),
                        Lifecycle = lifecycle
                    });
        }
...
    }

so that registration for each layer will look like:

    public class Registrator : ModuleRegistrator
    {
        protected override void Init()
        {
            Register<MainViewModel, IMainViewModel>();
            Register<UserEditViewModel, IUserEditViewModel>(Lifecycles.PerDependency);
            Register<UserDetailsViewModel, IUserDetailsViewModel>(Lifecycles.PerDependency);
            // and lots of similar registrations
        }
    }

Also you need to write some code transferring your registration declarations into actual registrations of IoC library you are using. For case of AutoFac this is:

   public static class AutoFacHelper
    {
        public static void Register<T>(ContainerBuilder builder)
            where T : ModuleRegistrator, new()
        {
            ModuleRegistrator moduleRegistrator = ModuleRegistrator.Create<T>();
            var registrationInfos = moduleRegistrator.Registrations;
            foreach (var registrationInfo in registrationInfos)
            {
                IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle> tempReg;
 
                if (registrationInfo.Key != null)
                {
                    tempReg = builder.RegisterType(registrationInfo.Implementation)
                        .Keyed(registrationInfo.Key, registrationInfo.Interface);
                }
                else
                {
                    tempReg = builder.RegisterType(registrationInfo.Implementation).As(registrationInfo.Interface);
                }
 
                switch (registrationInfo.Lifecycle)
                {
                    case Lifecycles.PerDependency:
                        tempReg.InstancePerDependency();
                        break;
                    case Lifecycles.PerScope:
                        tempReg.InstancePerLifetimeScope();
                        break;
                    case Lifecycles.Singleton:
                        tempReg.SingleInstance();
                        break;
                }
            }
        }
    }

Now, at your composition root, just register your layers one by one.

        private static void RegisterTypes()
        {
            AutoFacHelper.Register<BusinessLogic.Registrator>(Builder);
            AutoFacHelper.Register<ViewModels.Registrator>(Builder);
            AutoFacHelper.Register<Data.Registrator>(Builder);
 
            AutoFacHelper.Register<Devices.Registrator>(Builder);
        }

The order is important here. Most of IoC libraries override declarations done earlier with declarations done later. So registrations within layers are just declaration of default behavior. After the registration section, you can just override implementation of certain interface into something completely custom.

Mocking layers

If for each layer you have a list of registrations obtainable from corresponding Registrator class, you can just get from its Registrations list all interfaces this layer exposes, and just mock them using any mocking library. In our sample we are using Moq library for this purpose. In a way similar to ModuleRegistrator, MockRegistrator class stores mockers for interfaces for the layer:

    public struct MockRegistrationInfo
    {
        public Type Interface { get; set; }
        public Lifecycles Lifecycle { get; set; }
        public Mock Mocker { get; set; }
    }

This way MockRegistrator is able to register mockers to AutoFac instead of actual implementations:

        public void Init(ContainerBuilder builder)
        {
            ...
            foreach (var mockRegistrationInfo in Registrations)
            {
                builder.RegisterInstance(mockRegistrationInfo.Mocker.Object).As(mockRegistrationInfo.Interface);
            }
        }

Mocking the whole layer this way is done with minimal effort:

    public class DataMockRegistrator : MockRegistrator
    {
        protected override void InitCore()
        {
            RegisterAllMocksForLayer(ModuleRegistrator.Create<Sample.Data.Registrator>());
        }
    }

And registration is very similar to the entry point of
application:

        private static void RegisterTypes()
        {
            AutoFacHelper.Register<BusinessLogic.Registrator>(Builder);
            AutoFacHelper.Register<ViewModels.Registrator>(Builder);
 
            DataMockRegistrator.Init(Builder);
            DevicesMockRegistrator.Init(Builder);
        }

Here two layers are mocked, and implementation of two other
layers are tested.

Testing without BDD

It is important to say that this approach is perfectly applicable to writing traditional tests without BDD, SpecFlow and Gherkin. To run a test, you need first to clean-up previous objects. Or just register everything again using RegisterTypes. Surprisingly, no issues with performance on hundreds and even thousands of tests.

Then, you need to set up conditions that are mocked. You are doing this by getting mocker for the interface you need from MockRegistrator, for example:

    var repository = Bootstrapper.DataMockRegistrator.GetMock<IPersonsRepository>();
    repository.Setup(x => x.Get()).Returns(new List<Person>() { new Person() { Name = "John" } });

After that, you just need to resolve actual implementations from
container and call them in tests:

    mainViewModel = Bootstrapper.MainScope.Resolve<IMainViewModel>();
    mainViewModel.SelectedPerson = mainViewModel.Persons.Where(p => p.Name == p0).FirstOrDefault();

And it really makes tests simpler. You don't need to worry when you are changing internal relationships of classes. You don't need to instantiate classes directly so you don't need to worry about providing constructor parameters. Your test just remains the same if the requirements and interfaces to mocked layers are unchanged.

Setting up infrastructure for BDD tests

But another important aspect of larger-scale tests is that they can be easily combined with actual requirements. They can be made behaviour-driven. And I would strongly recommend to do this. See the previous article for the detailed discussion on this topic. Here we'll just discuss details regarding combining this approach with BDD and SpecFlow.

Before each scenario, you need to cleanup your container to prevent mutual dependency of tests, which may cause instability in tests results:

        [BeforeScenario]
        public static void BeforeTestRun()
        {
            Bootstrapper.Init();
        }
 
        [AfterScenario]
        public static void Cleanup()
        {
            Bootstrapper.Cleanup();
        }

It is important to remember that in internal implementation of BDD tests, steps should be decoupled to reflect internal structure, not the scenario in each it was used. In perfect case, you should have a separate Step class for each interface you test or mock, and reuse this step as much as possible for different scenarios.

Let's see how we can do it in case of our approach. In case of mocked interface, you'll just get the corresponding mocker in constructor of step class:

    [Binding]
    public class CallDeviceSteps
    {
        private readonly Mock<ICallDeviceService> _service;
 
        public CallDeviceSteps()
        {
            _service = Bootstrapper.DevicesMockRegistrator.GetMock<ICallDeviceService>();
        }

and set conditions on this mocker, for example:

        [Given(@"the following persons are available")]
        public void GivenTheFollowingPersonsAreAvailable(Table table)
        {
            var persons = table.CreateSet<Person>();
            _repository.Setup(x => x.Get()).Returns(persons);
        }

or do some checks verifying the behavior:

        [Then(@"number ""(.*)"" is called")]
        public void ThenNumberIsCalled(string p0)
        {
            _service.Verify(t => t.DoCall(p0));
        }

Things are slightly more complicated when you are dealing with actual class and not a mocker. You can't resolve it too early because setting conditions is not finished. The way mockers work, at the moment when someone resolves mocked interface from a container, its configuring should be finished. So to resolve classes from tested layer in a constructor is not an option. This is too early. However, we can presume that when "Given" section is finished, all conditions are set. At least any well-behaving BDD scenario should work this way. So, we can just do the resolving after "Given" section, that is, in the beginning of "When" section, before actual actions start to happen:

        [BeforeScenarioBlock]
        public void BeforeScenarioBlock()
        {
            // tests configuration is finished
            if (ScenarioContext.Current.CurrentScenarioBlock == ScenarioBlock.When)
            {
                _mainViewModel = Bootstrapper.MainScope.Resolve<IMainViewModel>();
                _mainViewModel.Init();
            }
        }

This is a basic description of layer-based testing system, and what you need to establish it. Hope it will be helpful to someone. However this is not all. The next article will be about more complicated and long scenarios, and ways to handle them.

Note: This is an article about architectural approach, not a library you can just include. Yes, the sample contains some code snippets but they are not very important and not very complicated. Besides, most of them are specific to IoC library and mocking library which were used for this particular project (Autofac and Moq). After grasping the whole concept writing similar things for your project, your set of layers and your specific libraries is a trivial task.

Reusable code from this sample is in folders "Common" of projects "Sample.Infrastructure", "Sample.WPF" and "Sample.Tests". You should use nuget "Restore" to download related packages and make the sample work

AttachmentSize
sample.zip93.4 KB

Comments

Muchos Gracias for your forum.Much thanks again. Want more.

Muchos Gracias for your forum.Much thanks again. Want more