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
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