Dependency Injection the .NET Core way

Dependency Injection the .NET Core way

This entry is part 9 of 9 in the series Launching WebDrivers in .NET Core the easy way

I have been building up to this one for a long time. Since I started using .NET professionally almost exactly a year ago, I have used the Inversion of Control pattern in building my test suites. I would have set up dependency injection using a framework, but was discouraged from doing so as Autofac, that we use internally is far too heavyweight for my needs.

Fortunately .NET Core has native support for minimal dependency injection in the microsoft.extensions.dependencyinjection namespace, so let’s see how we go about employing it in tests.

A few posts ago, I demonstrated unit tests that I can switch to running as a visible system test by injecting a DefaultWebDriverFactory object instead of a FakeWebDriverFactory object. As both implement the IWebDriverFactory interface this is a prime example of how Dependency Injection is used in unit testing, using fake (mock or stub) objects to test the functioning of a single method.

NSTC Discount Banner
Hear me speak at the National Software Testing Conference 2019.

Dependency Injection (DI) explained

When writing many different tests, they may use many of the same test objects. We could:

  1. Hard code constructors for exactly the combination of objects to build in the [SetUp] method of each test.
  2. Build the entire collection of objects for every test in a single [OneTimeSetup] method in each test class and only use the ones that are needed.

The first requires a lot of cutting and pasting of code, and is painful to maintain e.g. when you require something new to construct one of the objects and have to alter the setup code in 20 test methods.

The second is easier to maintain as you will only ever have to alter one piece of code (per test class) as objects change. You could construct everything in one enormous Base Class but this will run more slowly, generating unneccesary objects for many tests.

As a third path I will be starting out using .NET Core’s native IoC container functionality to generate and use an IServiceProvider instance.

Building your ServiceProvider

Using a DI framework, you define how to generate objects in an object implementing the System.IServiceProvider interface.

Microsoft.Extensions.DependencyInjection.ServiceProvider

You start by defining the implementations you require for each interface or class into a ServiceCollection, and then

IServiceProvider provider = ServiceCollection.BuildServiceProvider(); 

generates your dependency injecting IoC container (IServiceProvider.) In the case of my WebDriverManager unit/system tests the following works.

using System;
using System;
using System.Reflection;
using AlexanderOnTest.NetCoreWebDriverFactory.DriverManager;
using AlexanderOnTest.NetCoreWebDriverFactory.DriverOptionsFactory;
using AlexanderOnTest.NetCoreWebDriverFactory.UnitTests.Settings;
using AlexanderOnTest.NetCoreWebDriverFactory.WebDriverFactory;
using AlexanderOnTest.WebDriverFactoryNunitConfig.TestSettings;
using Microsoft.Extensions.DependencyInjection;

namespace AlexanderOnTest.NetCoreWebDriverFactory.UnitTests.DriverManager.Tests.Dependencies
{
    internal static class DependencyInjector
    {
        public static IServiceProvider GetServiceProvider()
        {
            ServiceCollection services = new ServiceCollection();

            services.AddSingleton(WebDriverSettings.WebDriverConfiguration);

            services.AddSingleton(new DriverPath(Assembly.GetExecutingAssembly()));

            // Allow a setting to override the default FakeWebDriveFactory
            Type webDriverType = TestSettings.UseRealWebDriver
                ? typeof(DefaultWebDriverFactory)
                : typeof(FakeWebDriverFactory);

            services.AddSingleton(typeof(IDriverOptionsFactory), typeof(DefaultDriverOptionsFactory));
            services.AddSingleton(typeof(ILocalWebDriverFactory), typeof(DefaultLocalWebDriverFactory));
            services.AddSingleton(typeof(IRemoteWebDriverFactory), typeof(DefaultRemoteWebDriverFactory));
            services.AddSingleton(typeof(IWebDriverFactory), webDriverType);
            services.AddSingleton(typeof(IWebDriverManager), typeof(WebDriverManager));
            
            return services.BuildServiceProvider();
        }
    }
}

Note:

  • You can map specific implementations to Types (as WebDriverConfiguration above) or to interfaces.
  • When adding dependencies you define whether they will be singletons (one instance used always) or transient (new instance every time.) In the case above there is no need to generate new objects so each is set as a singleton.
  • It is possible to make the required implementation for a type conditional on a parameter, test setting or environment variable as I have done here with IWebDriverFactory.

Injecting Dependencies from the Provider

So the IoC container (IServiceProvider) now has a ‘list of recipes’ for object construction. When you need to inject the dependency you have a couple of options.

System.IServiceProvider

One of the simplest .NET interfaces, it defines a single method:

public object GetService (Type serviceType);

e.g.

provider.GetService((IMyClass) typeof(MyClass));

NOTE: the (non generic) GetService method returns an object hence the need for the ugly cast.

This is the simplest implementation of IServiceProvider but I really don’t like the required cast so want something better.

Microsoft.Extensions.DependencyInjection namespace

Fortunately, as well as providing a simple means to build an IServiceProvider instance the extensions also provide a much nicer generic GetService Extension method:

public static T GetService (this IServiceProvider provider);

If you are not familiar with Extension methods, the first parameter is the object from which you are calling it (in this case the IServiceProvider.) The thing that always catches me out however is that you must remember to add the extension namespace to the using block Again looking at the set up for my WebDriverManager Tests as an example:

using System;
..
using Microsoft.Extensions.DependencyInjection;
..

namespace AlexanderOnTest.NetCoreWebDriverFactory.UnitTests.DriverManager.Tests
{
    [Category("CI")]
    public class WebDriverManagerTests
    {
..
        [OneTimeSetUp]
        public void Prepare()
        {
            ILoggerRepository logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
            XmlConfigurator.Configure(logRepository, new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "log4net.config")));

            IServiceProvider provider = DependencyInjector.GetServiceProvider();

            WebDriverManager = provider.GetService<IWebDriverManager>();
        }
..
    }
}

So was it worth it? A Cost Benefit Analysis

Cost:

  • A new class and quite a lot of code
  • A small performace hit compared with building the objects directly inside the test setup

Benefits:

  • My test class setup is now independent of the implementations of the objects required. I could have multiple tests classes taking a container from a single place and only have one section of code to change when implementation details change. (e.g. new construction parameters)
  • I don’t have to explicitly instantiate constructor parameters in my test setup, the IoC container does it for me.
  • In the case of setting up the test framework itself, DI is rather overkill, but once we get into building the page objects for testing, setup methods can become huge and are tightly coupled to implementations instead of interfaces.
  • The real advantages of dependency injection become apparent once you start using assembly scanning to find the implementations to provide. Microsoft.Extensions.DependencyInjection does not include this capability so in a future post I will be looking at using Scrutor to do this for me.
  • Many other DI libraries are available that also have far more features depending on your needs. One of the benefits of writing against the native extensions is the ability to inject one of these others libraries instead if more features or performance is required.

In Summary

Progress made:

  • Dependency injection implmented using .NET Core native support

Lessons learnt:

  • Don’t forget to add using Microsoft.Extensions.DependencyInjection; when trying to use the generic injection methods
  • Debugging problems with your IoC container is not easy. Remember to ensure that you have bound your implementation to the correct type. Spotting that MyClassImplementation is bound to MyClassImplementation instead of IMyClassImplementation is not easy. (Yes I did have that problem!)

A reminder:

If you want to ask me a question, Twitter is undoubtedly the fastest place to get a response: My Username is @AlexanderOnTest so I am easy to find. My DMs are always open for questions, and I publicise my new blog posts there too.

[Edit 26/6/19 Moved into the new series: Launching WebDrivers in .NET Core the easy way]

Series Navigation<< Applying a .runsettings file from the command line in .NET Core
Comments are closed.