我继承了一个没有接口或抽象类的项目,即仅具体类,我想引入单元测试。这些类包含许多函数,包含业务逻辑和数据逻辑;打破每个SOLID规则(http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29)。
我有一个想法。我正在考虑为每个设计不良的类创建接口,公开所有功能。那么至少我可以模拟课程。
我对单元测试比较陌生(我有一个项目经验,在正确的地方使用接口非常好)。这样做是一个好主意,即为所有具体类创建接口(公开所有函数和子例程),仅用于单元测试?
我花了一些时间研究这个,但我没有找到答案。
答案 0 :(得分:5)
如果您的项目根本没有测试,那么在添加任何单元测试之前,我宁愿创建更高级别的测试(即验收,功能和/或集成测试)。
当您进行这些测试时,您知道系统的行为应该是它应该的,并且它具有一定程度的“外部”质量(意味着您的程序的输入和输出是预期的)。
一旦您的高级测试正常,您可以尝试将单元测试添加到已存在的类中。
我敢打赌,如果您希望能够对其进行单元测试,那么您将发现自己需要重构一些现有的类,这样您就可以将高级测试用作安全网会告诉你是否有任何损坏。
答案 1 :(得分:3)
这是一个难以解决的问题。我认为你走在正确的轨道上。你最终会得到一些丑陋的代码(例如为每个单片类创建header interfaces),但这应该只是一个中间步骤。
我建议投资一份Working Effectively with Legacy Code。首先,您可以先阅读this distillation。
除了Karl的选项(允许你通过拦截进行模拟)之外,你还可以使用Microsoft Fakes & Stubs。但是这些工具不会鼓励您重构代码以遵守SOLID原则。
答案 2 :(得分:2)
是的,这是一个良好的开端,但是,接口不是注入依赖项的优先级。如果所有遗留类都获得了接口,但内部隐藏它们仍然是相互依赖的,那么这些类仍然不容易测试。例如,假设你有两个看起来像这样的类:
Public Class LegacyDataAccess
Public Function GetAllSales() As List(Of SaleDto)
' Do work with takes a long time to run against real DB
End Function
End Class
Public Class LegacyBusiness
Public Function GetTotalSales() As Integer
Dim dataAccess As New LegacyDataAccess()
Dim sales As List(Of SaleDto) = dataAccess.GetAllSales()
' Calculate total sales
End Function
End Class
我知道你在说什么...... “我希望遗留代码至少能够很好地分层”,但是让我们用它作为一些遗留代码的例子很难去测试。难以测试的原因是代码到达数据库并对数据库执行耗时的查询,然后从中计算结果。因此,为了在当前状态下测试它,您需要首先将一堆测试数据写入数据库,然后运行代码以查看它是否根据插入的数据返回正确的结果。必须编写类似的测试是有问题的,因为:
正如您所正确观察的那样,接口对于单元测试非常重要。因此,正如您所建议的那样,我们可以添加接口以查看它是否更容易测试:
Public Interface ILegacyDataAccess
Function GetAllSales() As List(Of SaleDto)
End Interface
Public Interface ILegacyBusiness
Function GetTotalSales() As Integer
End Interface
Public Class LegacyDataAccess
Implements ILegacyDataAccess
Public Function GetAllSales() As List(Of SaleDto) _
Implements ILegacyDataAccess.GetAllSales
' Do work with takes a long time to run against real DB
End Function
End Class
Public Class LegacyBusiness
Implements ILegacyBusiness
Public Function GetTotalSales() As Integer _
Implements ILegacyBusiness.GetTotalSales
Dim dataAccess As New LegacyDataAccess()
Dim sales As List(Of SaleDto) = dataAccess.GetAllSales()
' Calculate total sales
End Function
End Class
所以现在我们有接口,但实际上,它如何使它更容易测试?现在我们可以轻松地创建一个模拟数据访问对象,它实现了相同的接口,但这并不是核心问题。问题是,我们如何让业务对象使用模拟数据访问对象而不是真实对象?要做到这一点,您需要通过引入依赖注入将重构提升到一个新的水平。真正的罪魁祸首是业务类的以下行中的New
关键字:
Dim dataAccess As New LegacyDataAccess()
业务类显然取决于数据访问类,但目前它隐藏了这一事实。这是关于它的依赖性。它说,来吧,这很容易,只需调用此方法,我将返回结果 - 这就是所需要的。真的,它需要更多。现在,让我们说,我们阻止它说谎它的依赖关系并使它如此毫不掩饰地说出来,就像这样:
Public Class LegacyBusiness
Implements ILegacyBusiness
Public Sub New(dataAccess As ILegacyDataAccess)
_dataAccess = dataAccess
End Sub
Private _dataAccess As ILegacyDataAccess
Public Function GetTotalSales() As Integer _
Implements ILegacyBusiness.GetTotalSales
Dim sales As List(Of SaleDto) = _dataAccess.GetAllSales()
' Calculate total sales
End Function
End Class
现在,正如您所看到的,此类很多更容易测试。我们不仅可以轻松创建模拟数据访问对象,而且现在我们可以轻松地将模拟数据访问对象注入到业务对象中。现在我们可以创建一个模拟器,它可以快速轻松地返回我们想要返回的数据,然后查看业务类是否返回正确的计算 - 不涉及数据库。
不幸的是,虽然向现有类添加接口是轻而易举的,但重构它们以使用依赖注入通常需要更多的工作。您可能需要计划哪些类最有意义才能首先解决。您可能需要创建一些中间旧学校包装器,它们按照代码的方式工作,因此您在重构代码的过程中不会破坏现有代码。这不是一件快速而简单的事情,但是如果你有耐心并长期坚持下去,就有可能做到这一点,你会很高兴你做到了。
答案 3 :(得分:1)
答案 4 :(得分:0)
创建用于测试类的接口并不是一个坏主意 - 单元测试的目标是在类上的函数按预期运行时进行练习。根据您正在使用的类,这说起来容易做起来难 - 如果对全局状态有很多依赖,等等,您需要相应地进行模拟。
考虑到有价值的单元测试,将一些工作放入其中(达到极限)将使您和与之合作的开发人员受益。
答案 5 :(得分:0)
我更喜欢创建接口和类,因为您需要测试的东西而不是所有的东西。
除了接口之外,您还可以使用一些技术来测试遗留代码。我经常使用的是“Extract And Override”,你在其他方法中提取一些“不可测试”的代码,并使其可以覆盖。它们派生出你想要测试的类,并用一些传感代码覆盖“untestable”方法。
使用模拟框架就像在方法中添加关键字Overridable一样简单,并使用模拟框架设置返回。
您可以在“Working Effectively with Legacy Code”一书中找到许多技巧。
关于现有代码的一件事是,有时候编写集成测试比单元测试更好。在您测试完行为后,您将创建单元测试。
另一个提示是从具有较少依赖性的模块/类开始,这样,您就可以轻松熟悉代码。
如果您需要一个关于“提取和覆盖”的示例,请告诉我;)