Thursday, September 10, 2020

Daisy-chaining dependency injection via events

In my earlier post dependency-injection-using-events-to.html I examined how we could do dependency injection via events. In reality we need to inject dependencies into objects created by objects we directly instantiate. For example, we instantiate a configuration object and use dependency injection to tell it to instantiate a particular data provider. But that data provider needs to instantiate a specific logging object, etc. In a real life scenario there can be a dozen objects that need to be injected which gets complicated.

By using events to perform only-if-needed dependency injection, we can daisy chain them. So when the data provider needs to log something it can ask the configuration object what logging mechanism to use and the configuration object simply asks the calling object and passes the answer back down. This plumbing can be hidden in base classes.

The calling object or test runner needs to provide an event handler to specify the data provider and another for the logger. We can daisy-chain properties or constructors too which I will do in later posts.

Start a new C# .Net Core console app called EventsFromBaseClass. Add a new class called LoggingProvider.cs. We will put a LoggingProvider base class and a Production and Test derived class here. I could have used an interface instead of a base class, but the data provider will need an abstract base class so I decided to use one here too for consistency. I also define the event args that is used to pass the logging provider instance back from the event handler.

using System;
 
namespace EventsFromBaseClass
{
    public class ProvideLoggingProviderEventArgs : EventArgs
    {
        public BaseLoggingProvider loggingProvider;
    }
 
    public  abstract class BaseLoggingProvider
    {
        public abstract string GetName();
        public abstract void Log(string s);
    }
 
    public class ProductionLoggingProvider : BaseLoggingProvider
    {
        public override string GetName()
        {
            return "Production logging provider";
        }
 
        public override void Log(string s)
        {
            // Write to a log file
        }
    }
 
    public class TestLoggingProvider : BaseLoggingProvider
    {
        public override string GetName()
        {
            return "Test logging provider";
        }
 
        public override void Log(string s)
        {
            Console.WriteLine(s);
        }
    }
}

Now we can define our data providers. The data providers will need to ask the caller which kind of logging provider they should use. So they define a ProvideLoggingProvider event. The data providers expose some typical data provider functionality. In addition, the LoggingProvider property will raise the ProvideLoggingProvider event if it needs to do some logging and doesn't currently have a logging provider. This will ask the calling object what kind of logging provider it should use.

using System;

using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
 
namespace EventsFromBaseClass
{
    public class ProvideDataProviderEventArgs : EventArgs
    {
        public BaseDataProvider dataProvider;
    }
 
    public abstract class BaseDataProvider
    {
        public event EventHandler<ProvideLoggingProviderEventArgs> ProvideLoggingProvider;
 
        private BaseLoggingProvider _LoggingProvider = null;
        public BaseLoggingProvider LoggingProvider  // In real life this is protected
        {
            get
            {
                if (_LoggingProvider == null)
                    if (ProvideLoggingProvider == null)
                        _LoggingProvider = new ProductionLoggingProvider();
                    else
                    {
                        ProvideLoggingProviderEventArgs e = new ProvideLoggingProviderEventArgs();
                        ProvideLoggingProvider.Invoke(this, e);
                        _LoggingProvider = e.loggingProvider;
                    }
                return _LoggingProvider;
            }
        }
 
        public abstract void OpenConnection();
        public abstract void CloseConnection();
        public abstract Settings GetSettings();
        public abstract bool IsConnectionOpen();
        public abstract string GetName();
    }
 
    public class ProductionDataProvider : BaseDataProvider
    {
        private SqlConnection sqlConnection = null;
 
        public override void CloseConnection()
        {
            LoggingProvider.Log("Closing connection");
            sqlConnection.Close();
            sqlConnection = null;
        }
 
        public override Settings GetSettings()
        {
            LoggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { IsProductionMode = true };  // This would be read from the database
            else
                throw new Exception("Connection is not open");
        }
 
        public override bool IsConnectionOpen()
        {
            return (sqlConnection != null && sqlConnection.State == ConnectionState.Open);
        }
 
        public override void OpenConnection()
        {
            LoggingProvider.Log("Opening connection");
            sqlConnection = new SqlConnection("Server=(local);database=master;Trusted_Connection=true");
            sqlConnection.Open();
        }
 
        public override string GetName()
        {
            return "Production data provider";
        }
    }
 
    public class TestDataProvider : BaseDataProvider
    {
        private bool isConnectionOpen = false;
 
        public override void CloseConnection()
        {
            LoggingProvider.Log("Closing connection");
            isConnectionOpen = false;
        }
 
        public override Settings GetSettings()
        {
            LoggingProvider.Log("Getting settings");
            if (IsConnectionOpen())
                return new Settings() { IsProductionMode = false };
            else
                throw new Exception("Connection is not open");
        }
 
        public override bool IsConnectionOpen()
        {
            return isConnectionOpen;
        }
 
        public override void OpenConnection()
        {
            LoggingProvider.Log("Opening connection");
            isConnectionOpen = true;
        }
 
        public override string GetName()
        {
            return "Test data provider";
        }
    }
}

The configuration object just has the task of fetching a Settings object. We don't need production and test versions of this - it's the class we're testing. However, we do need to tell it what type of data provider and what type of logging provider to use. We will do this when it raises ProvideDataProvider and ProvideLoggingProvider events. It can ask for a logging provider either when it needs one or when its data provider asks for one. Look at the DataProvider and LoggingProvider properties.


using System;
 
namespace EventsFromBaseClass
{
    public class Settings
    {
        public bool IsProductionMode;
    }
 
    public class Configuration
    {
        public event EventHandler<ProvideDataProviderEventArgs> ProvideDataProvider;
        public event EventHandler<ProvideLoggingProviderEventArgs> ProvideLoggingProvider;
 
        private BaseDataProvider _DataProvider = null;
        public BaseDataProvider DataProvider    // In real life this is protected or private
        {
            get
            {
                if (_DataProvider == null)
                    if (ProvideDataProvider == null)
                        _DataProvider = new ProductionDataProvider();
                    else
                    {
                        ProvideDataProviderEventArgs e = new ProvideDataProviderEventArgs();
                        ProvideDataProvider.Invoke(this, e);
                        _DataProvider = e.dataProvider;
                    }
                _DataProvider.ProvideLoggingProvider += Provider_ProvideLoggingProvider;
                return _DataProvider;
            }
        }
 
        private BaseLoggingProvider _loggingProvider = null;
        private void Provider_ProvideLoggingProvider(object sender, ProvideLoggingProviderEventArgs e)
        {
            if (_loggingProvider == null)
            {
                if (ProvideLoggingProvider == null)
                    _loggingProvider = new ProductionLoggingProvider();
                else
                {
                    // Pass it up the chain
                    ProvideLoggingProviderEventArgs e2 = new ProvideLoggingProviderEventArgs();
                    ProvideLoggingProvider.Invoke(this, e2);
                    _loggingProvider = e2.loggingProvider;
                }
            }
            e.loggingProvider = _loggingProvider;
        }
 
        public Settings GetSettings()
        {
            Settings settings;
 
            DataProvider.OpenConnection();
            settings =  DataProvider.GetSettings();
            DataProvider.CloseConnection();
            return settings;
        }
    }
}

Now it is time to do some dependency injection. Alter program.cs to look like this. You can see we define two event handlers. If you put a break point on Configuration_ProvideDataProvider you will see the data provider asks configuration what kind of logging provider it should use and configuration asks us.


using System;
 
namespace EventsFromBaseClass
{
    class Program
    {
        static void Main(string[] args)
        {
            bool isProductionMode = false;
 
            Configuration configuration = new Configuration();
            configuration.ProvideDataProvider += Configuration_ProvideDataProvider;
            configuration.ProvideLoggingProvider += Configuration_ProvideLoggingProvider;
 
            isProductionMode = configuration.GetSettings().IsProductionMode;
 
            Console.WriteLine(string.Format("isProductionMode = {0}", isProductionMode));
            Console.WriteLine(string.Format("Data Provider name is {0}", configuration.DataProvider.GetName()));
            Console.WriteLine(string.Format("Logging name is {0}", configuration.DataProvider.LoggingProvider.GetName()));
        }
 
        private static void Configuration_ProvideLoggingProvider(object sender, ProvideLoggingProviderEventArgs e)
        {
            e.loggingProvider = new ProductionLoggingProvider();
        }
 
        private static void Configuration_ProvideDataProvider(object sender, ProvideDataProviderEventArgs e)
        {
            e.dataProvider = new ProductionDataProvider();
        }
    }
} 

Although there is quite a lot of wiring up required, it is mostly in the base classes so the inherited classes can be kept focused on doing what they were written to do. If you run the program now you will see the production providers are being used.



Now it is time to write a test that injects test classes. Add an NUnit test project to the solution and call it EventsFromBaseClassTest. Add a reference to the EventsFromBaseClassProject. Rename the class ConfigurationTest and make the code look like this...

using NUnit.Framework;
using EventsFromBaseClass;
using System;
 
namespace EventsFromBaseClassesTest
{
    public class Tests
    {
        [Test]
        public void Test1()
        {
            bool isProductionMode = false;
            Configuration configuration = new Configuration();
            configuration.ProvideDataProvider += Configuration_ProvideDataProvider;
            configuration.ProvideLoggingProvider += Configuration_ProvideLogging;
 
            isProductionMode = configuration.GetSettings().IsProductionMode;
 
            Assert.IsFalse(isProductionMode);
            Assert.AreEqual("Test data provider", configuration.DataProvider.GetName());
            Assert.AreEqual("Test logging provider", configuration.DataProvider.LoggingProvider.GetName());
        }
 
        private static void Configuration_ProvideLogging(object sender, ProvideLoggingProviderEventArgs e)
        {
            e.loggingProvider = new TestLoggingProvider();
        }
 
        private static void Configuration_ProvideDataProvider(object sender, ProvideDataProviderEventArgs e)
        {
            e.dataProvider = new TestDataProvider();
        }
    }
}

The test defines Configuration and the event handlers in the same way as Program.cs but the event handlers return test providers instead of production providers. Our asserts pass, which tells us the test providers are being used.

If you run the test it will pass.







Thursday, September 3, 2020

Dependency Injection using events to help unit testing

We're trying to get into Unit Testing and we have discovered many of our methods cannot be unit tested because we wrote them wrong. I don't think we're the first to make this discovery. There are far too many hard-wired dependencies. We could fake and shim our way around some of the problems, but that makes the tests extremely complex.

One of the worst offenders are methods that instantiate other classes such as data providers. Suppose we have a method that closes a purchase order. It instantiates a data provider that knows how to read and update a purchase order, how to update the accounts tables, etc. If we want to test that method, it will instantiate this data provider and it's going to access the database but we don't want it to because accessing a database is slow and the data needs to be cleaned up after the test. We need to tell ClosePurchaseOrder to use a different data provider that just simulates the database calls. Perhaps one that was written specifically for this test and only implements the methods needed by the test and implements them in memory.

If you are building your application with unit testing in mind you can use dependency injection to pass a data provider to the ClosePurchaseOrder method or the PurchaseOrder class constructor. However, if you also need to pass a Configuration object and a Logging object, etc, then the argument lists start getting unwieldy. Plus you start getting very repetitive code wherever the PurchaseOrder class is instantiated.

One option is just-in-time dependency injection. When the ClosePurchaseOrder method needs to access the database it calls a method in its base class that either returns the default data provider or raises an event that allows the calling code to provide a custom data provider. The result is cached. Either way, the ClosePurchaseOrder method gets a data provider. It doesn't care which one, or how. One advantage to using events to get dependencies is that if we follow a code path that does not need a dependency, we will not create it.

If we are going to substitute one data provider for another we need to have a data provider interface defined. This is core to all forms of dependency injection.

Here is a walk through that shows how this all ties together. It has a class that updates something via a data provider. By default we want it to use the default data provider. We want to be able to write a test for that class but use a test data provider for the test. We will use NUnit for our test framework.

Start a new Visual Basic console .Net Core project called ConsoleDIWithEvents.

As always, we start with the interface. Add an interface file called IDataProvider with this interface defined.

Public Interface IDataProvider
    Function DoUpdate(SQL As String) As Integer
End Interface


Now add a class for our default data provider. Call it DefaultDataProvider

Public Class DefaultDataProvider
    Implements IDataProvider

    Public Function DoUpdate(SQL As String) As Integer Implements IDataProvider.DoUpdate
        Console.WriteLine("Production update")
        Return 1
    End Function
End Class


and add a class for the test data provider called TestDataProvider

Public Class TestDataProvider
    Implements IDataProvider

    Public Function DoUpdate(SQL As String) As Integer Implements IDataProvider.DoUpdate
        Console.Write("Test update")
        Return 0
    End Function
End Class


They return different values so we can tell which actually got called for the purpose of the walk through. Normally you would return whatever makes sense.

The functionality to decide how to get the data provider will be in a base class. I lean towards creating events and properties for each type of dependency rather that trying to make this code generic. The DataProvider property's get method detects if we already have a data provider and, if not, checks to see if the owning code has provided an event handler. If so, the event is raised and we use the provider that is returned in the event args, otherwise we use a new default provider. We will see an example of the owning code providing an event handler when we write the unit test.

Public Class BaseClass

    Public Event ProvideDataProvider As EventHandler(Of ProvideDataProviderEventArgs)
    Public Class ProvideDataProviderEventArgs
        Inherits EventArgs
        Public DataProvider As IDataProvider
    End Class

    Private _DataProvider As IDataProvider = Nothing
    Protected ReadOnly Property DataProvider As IDataProvider
        Get
            If _DataProvider Is Nothing Then
                If ProvideDataProviderEvent Is Nothing Then
                    _DataProvider = New DefaultDataProvider()
                Else
                    Dim e As New ProvideDataProviderEventArgs()
                    RaiseEvent ProvideDataProvider(Me, e)
                    _DataProvider = e.DataProvider
                End If
            End If
            Return _DataProvider
        End Get
    End Property
End Class


Now we finally get to some "real code". Add a class called PurchaseOrder. This is where we would put ClosePurchaseOrder and other code that implements the business logic for purchase orders. It gets the data provider using the DataProvider property and then executes a method on it. The data provider that is executed depends on whether an event handler has been written.

Public Class PurchaseOrder
    Inherits BaseClass

    Public Function ClosePurchaseOrder() As Integer
        Return DataProvider.DoUpdate("X")
    End Function
End Class


The only thing left to do for our production code is to instantiate PurchaseOrder and call ClosePurchaseOrder from the console. Change Program to look like this.

Module Program
    Dim purchaseOrder As PurchaseOrder
    Sub Main(args As String())
        Dim UpdateResult As Integer
        purchaseOrder = New PurchaseOrder()
        UpdateResult = purchaseOrder.ClosePurchaseOrder()
        Console.WriteLine(String.Format("Close returned result {0}", UpdateResult))
    End Sub
End Module


If you run this you can see the PurchaseOrder class uses the default data provider.


All that might seem like a very complicated way to do something that could be much easier. So let's see why we did this.

Add a new NUnit test project to the solution. Call it ConsoleDIWithEventsTest. Rename UnitTest1 to PurchaseOrderTest. Add a reference to ConsoleDIWithEvents. Your solution now looks like this.



We are going to write a unit test for ClosePurchaseOrder but we want the test to use the test data provider instead of the default one. We do that by providing an event handler for ProvideDataProvider. The test looks like this.

Imports ConsoleDIWithEvents
Imports ConsoleDIWithEvents.BaseClass
Imports NUnit.Framework

Namespace ConsoleDIWithEventsTest
    Public Class PurchaseOrderTests

        Dim WithEvents purchaseOrder As PurchaseOrder

        <Test>
        Public Sub ClosePurchaseOrderTest()
            Dim UpdateResult As Integer
            purchaseOrder = New PurchaseOrder()
            UpdateResult = purchaseOrder.ClosePurchaseOrder()
            Assert.AreEqual(0, UpdateResult)
        End Sub

        Private Sub ProvideTestDataProvider(sender As Object, e As ProvideDataProviderEventArgs) Handles purchaseOrder.ProvideDataProvider
            e.DataProvider = New TestDataProvider()
        End Sub
    End Class
End Namespace


If you run the test, you can see ClosePurchaseOrder returns zero which means the test data provider was used because the test passed.


Lets's check that removing the event handler causes the default data provider to be used which will cause the test to fail. Comment out the event handler.

        'Private Sub ProvideTestDataProvider(sender As Object, e As ProvideDataProviderEventArgs) Handles mt.ProvideDataProvider
        '    e.DataProvider = New TestDataProvider()
        'End Sub



So we have an easy way to Mock using dependency injection. Once the plumbing is written our code simply uses base class properties to access dependencies (a good technique anyway) and the test code writes an event handler to provide the dependency on demand. Production code does nothing special. Plus, events provide an only-if-needed mechanism for dependency injection.