Reputation: 1637
I am new to Dependency Injection (DI) and Unit Testing. I successfully followed a tutorial example to create an MVC app that uses DI to loosely couple the code. The primary DI was to create a concrete SQL Repository to be passed in to the Controllers which then pass the Repository to the Domain layer, loosely coupling the Domain layer from the UI/Presentation Layer and from the Data Access layer. That worked OK. It was complicated because it required the following:
'This custom Controller Factory is used to create Controllers so that required Dependency Injection parameters can be passed in
Public Class CommerceControllerFactory
Inherits DefaultControllerFactory
Private ReadOnly controllerMap As Dictionary(Of String, Func(Of RequestContext, IController))
Public Sub New(repository As DomainLyrDi02Commerce.Domain.ProductRepository)
If repository Is Nothing Then
Throw New ArgumentNullException("repository")
End If
controllerMap = New Dictionary(Of String, Func(Of RequestContext, IController))()
controllerMap("Account") = Function(ctx) New AccountController()
controllerMap("Home") = Function(ctx) New HomeController(repository)
End Sub
Public Overrides Function CreateController(requestContext As RequestContext, controllerName As String) As IController
Return controllerMap(controllerName)(requestContext)
End Function
Public Overrides Sub ReleaseController(controller As IController)
End Sub
Protected Overrides Function GetControllerInstance(requestContext As RequestContext, controllerType As Type) As IController
Dim connectionString As String = ConfigurationManager.ConnectionStrings("CommerceObjectDbConnection").ConnectionString
Dim productRepository = New SqlDataAccessLyrDi02Commerce.DataAccess.SqlProductRepository(connectionString)
If controllerType = GetType(HomeController) Then
Return New HomeController(productRepository)
End If
Return MyBase.GetControllerInstance(requestContext, controllerType)
End Function
End Class
The MVC setup also required this:
'This is where the concrete SQLProductRepository is instantiated for use in the Controller Factory
Public Class CompositionRoot
Private ReadOnly m_controllerFactory As IControllerFactory
Public Sub New()
m_controllerFactory = CompositionRoot.CreateControllerFactory()
End Sub
Public ReadOnly Property ControllerFactory() As IControllerFactory
Get
Return m_controllerFactory
End Get
End Property
Private Shared Function CreateControllerFactory() As IControllerFactory
Dim connectionString As String = ConfigurationManager.ConnectionStrings("CommerceObjectDbConnection").ConnectionString
Dim productRepositoryType = GetType(DataAccess.SqlProductRepository)
Dim repository = DirectCast(Activator.CreateInstance(productRepositoryType, connectionString), DataAccess.SqlProductRepository)
Dim controllerFactory = New CommerceControllerFactory(repository)
Return controllerFactory
End Function
End Class
'The CompositionRoot is called from this code in the global.asax when registering the custom Controller Factory above
Dim root = New CompositionRoot
ControllerBuilder.Current.SetControllerFactory(root.ControllerFactory)
All this worked nicely. Now I want to unit test. Using HomeController as a sample piece of potential presentation layer logic, my first obstacle is just to begin I will need to create a New HomeController in the Test Method. But that requires a SQLProductRepository to be passed in that doesn’t actually contact the SQL server, which would be an integration test. But in order to substitute a fake I'll have to create a seam in the code, but I don’t understand how to set that up in this situation. I think it requires a separate DI, but since I’m not that sure-footed with DI just yet, I’m not certain how to do this.
This code is from the Data Access layer. It actually has 2 changes in it that are my first 2 steps toward setting up the DI for the SqlProductRepository. I changed:
DbSet to IDbSet
Extracted the Interface ICommerceObjectContext from CommerceObjectContext
.
Public Interface ICommerceObjectContext
Property ProductsInSql As IDbSet(Of Product)
End Interface
'This is the class used for the code first EF to SQL connection
Public Class Product
Public Property ProductId As Integer
Public Property name As String
Public Property UnitPrice As Decimal
Public Property IsFeatured As Boolean
End Class
Public Class CommerceObjectContext
Inherits DbContext
Implements ICommerceObjectContext
Public Sub New()
MyBase.New("CommerceObjectDbConnection")
End Sub
Public Sub New(connectionString As String)
MyBase.New(connectionString)
End Sub
Public Property ProductsInSql As IDbSet(Of Product) Implements ICommerceObjectContext.ProductsInSql
End Class
Finally, this is the Repository I need to work with.
Public Class SqlProductRepository
Inherits Domain.ProductRepository
Private ReadOnly context As CommerceObjectContext
Public Sub New(connectionString As String)
context = New CommerceObjectContext(connectionString)
End Sub
Public Overrides Function GetFeaturedProducts() As IEnumerable(Of Domain.Product)
Dim products = (From p In context.ProductsInSql Where p.IsFeatured Select p).AsEnumerable()
Return From p In products Select p.ToDomainProduct()
End Function
End Class
From what I have read, I think the next step is to inject the CommerceObjectContext dependency into the code somewhere to create a new seam, but I don’t understand how that is done. It seems more complicated because I’m actually creating the concrete instance in CompositionRoot and that itself is part of a DI.
It may be there are better ways to do DI with an MVC project, but I’m doing this to learn DI, so I would like to know at least how to accomplish the DI to enable the application of a fake in the unit tests.
What are my next steps just to get the production code dependency injected, so I can then create the unit tests correctly? While I might eventually need help with the unit test, I have to get the code ready first.
Upvotes: 0
Views: 134
Reputation: 56909
create a concrete SQL Repository to be passed in to the Controllers which then pass the Repository to the Domain layer, loosely coupling the Domain layer from the UI/Presentation Layer and from the Data Access layer.
Many people struggle with this at first. DI is about injecting object graphs. This means that any dependency your MVC controllers have can in turn be injected by dependencies, which can in turn be injected by dependencies, etc. To create a loosely-coupled application, your controller should know nothing about the dependencies of dependencies, only dependencies of itself. You would not pass a repository to the controller, but pass it only to the services that require it.
Public Class ProductController
Inherits Controller
Private ProductService As IProductService
Public Sub New(productService As IProductService)
Me.ProductService = productService
End Sub
Public Function Index() As ActionResult
Dim Model As IEnumerable(Of Domain.Product) = Me.ProductService.GetFeaturedProducts();
Return View(Model)
End Function
End Class
Public Class ProductService
Private ProductRepository As IProductRepository
' NOTE: There is some debate whether a repository is worth the effort.
' My view is that you should just make a single generic repository
' with a common set of CRUD methods that can manipulate any table (DRY)
' and any other query that doesn't conform to this strict model should
' be its own separate service.
Public Sub New(productRepository As IProductRepository) ' Alternative: IRepository(Of Domain.Product)
Me.ProductRepository = productRepository
End Sub
' Implement service methods that use the ProductRepository and/or DBContext directly
End Class
The magic that happens to make your services injectable, also makes them testable. It is usually a waste of effort to make unit tests use a DI container. Instead, you should new up the dependencies for each test or group of tests.
<Test> _
Public Sub TestGetFeaturedProducts
'Arrange
Dim mockRepository As IProductRepository = New Mock(Of IProductRepository)
' mockRepository.SetUp() ' Setup the repository to return fake data
Dim target As ProductService = New ProductService(mockRepository.Object)
'Act
Dim result As IEnumerable(Of Domain.Product) = target.GetFeaturedProducts()
'Assert
Assert.AreEqual(3, result.Count())
Assert.AreEqual("Product1", result.ElementAt(0).Name)
' Assert the rest of the data set to ensure it is what was setup in the above mock
End Sub
NOTE: It is also common practice to inject the DI container into the
ControllerFactory
so it can callcontainer.Resolve(Type)
. This is OK if theControllerFactory
is part of your composition root (meaning it should live in the MVC project). This allows theControllerFactory
to resolve the requested controller along with its entire dependency graph. There is an example of aControllerFactory
implementation in this article.
NOTE:
IProductService
is probably too general for a real application. Services that are suffixed withService
orManager
are code smells that indicate violations of the Single Responsibility Principle.For example, if using CQS, there might just be a service called
IQueryHandler<GetFeaturedProducts>
that depends on aDBContext
orDBContextFactory
that only has a singleHandle(GetFeaturedProducts)
method. In general it is better for maintainability to have a complex network of simple classes than a simple network of complex classes.
Upvotes: 1