我是依赖注入(DI)和单元测试的新手。我成功地按照教程示例创建了一个使用DI来松散耦合代码的MVC应用程序。主要的DI是创建一个具体的SQL存储库,传递给控制器,然后将存储库传递给域层,松散地将UI层与UI /表示层和数据访问层耦合。这很好用。这很复杂,因为它需要以下内容:
'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
MVC设置还要求:
'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)
这一切都很顺利。现在我想进行单元测试。使用HomeController作为潜在表示层逻辑的示例,我的第一个障碍就是开始我将需要在测试方法中创建一个新的HomeController。但是这需要传入一个实际上不会联系SQL服务器的SQLProductRepository,这将是一个集成测试。但是为了替换假的我必须在代码中创建一个接缝,但我不明白如何在这种情况下设置它。我认为这需要一个单独的DI,但由于我还没有确定DI,我不确定如何做到这一点。
此代码来自数据访问层。它实际上有2个变化,这是我为SqlProductRepository设置DI的前两个步骤。我改变了:
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
最后,这是我需要使用的存储库。
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
从我读过的内容来看,我认为下一步是将CommerceObjectContext依赖项注入到某个地方的代码中以创建一个新的接缝,但我不明白它是如何完成的。这似乎更复杂,因为我实际上是在CompositionRoot中创建具体实例,而且它本身也是DI的一部分。
可能有更好的方法与MVC项目进行DI,但我这样做是为了学习DI,所以我想知道至少如何完成DI以启用假的应用程序单元测试。
为了获得生产代码依赖项,我接下来的步骤是什么,以便我可以正确地创建单元测试?虽然我最终可能需要单元测试的帮助,但我必须先准备好代码。
答案 0 :(得分:1)
创建一个具体的SQL存储库,传入控制器,然后将存储库传递给域层,松散地将UI层与UI /表示层和数据访问层耦合。
许多人起初都在为此而斗争。 DI是关于注入对象图。这意味着你的MVC控制器所依赖的任何依赖关系都可以依次注入依赖关系,依赖关系也可以依赖注入依赖关系等。要创建一个松散耦合的应用程序,你的控制器应该对依赖关系的依赖性一无所知,只有依赖关系。本身。您不会将存储库传递给控制器,而只是将其传递给需要它的服务。
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
使您的服务可注入的魔法也使它们可测试。使单元测试使用DI容器通常是浪费精力。相反,您应该为每个测试或一组测试创建依赖项。
<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
注意:将DI容器注入
ControllerFactory
也是一种常见做法,因此可以调用container.Resolve(Type)
。如果ControllerFactory
是组合根的一部分(意味着它应该存在于MVC项目中),这是可以的。这允许ControllerFactory
解析请求的控制器及其整个依赖关系图。 this article中有一个ControllerFactory
实施示例。
注意:
IProductService
对于实际应用来说可能过于笼统。带有Service
或Manager
后缀的服务是代码气味,表明违反了单一责任原则。例如,如果使用CQS,可能只有一个名为
IQueryHandler<GetFeaturedProducts>
的服务取决于只有DBContext
方法的DBContextFactory
或Handle(GetFeaturedProducts)
。一般来说,可维护性比复杂类的简单网络具有复杂的简单类网络更好。