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.

No comments:

Post a Comment