5

我继承了一个没有接口或抽象类的项目,即只有具体类,我想介绍单元测试。类包含很多功能,其中包含业务逻辑和数据逻辑;打破 SOLID 的所有规则(http://en.wikipedia.org/wiki/SOLID_%28object-orientated_design%29)。

我有一个想法。我正在考虑为每个设计不佳的类创建接口,公开所有功能。然后至少我可以模拟这些课程。

我对单元测试比较陌生(我有一个项目的经验,该项目在正确的地方使用接口开发得非常好)。这样做是否是个好主意,即为所有具体类创建接口(公开所有函数和子例程),仅用于单元测试?

我花了一些时间研究这个,但我还没有找到答案。

4

6 回答 6

5

如果您的项目根本没有测试,那么在添加任何单元测试之前,我宁愿创建更高级别的测试(即验收、功能和/或集成测试)。

当您进行这些测试时,您就知道系统正在按应有的方式运行,并且它具有一定程度的“外部”质量(这意味着程序的输入和输出是预期的)。

一旦您的高级测试开始工作,您可以尝试将单元测试添加到已经存在的类中。

我敢打赌,如果你想对现有的一些类进行单元测试,你会发现自己需要重构它们,这样你就可以使用高级测试作为安全网,告诉你是否破坏了任何东西。

于 2013-08-26T18:37:36.570 回答
3

这是一件很难解决的事情。我认为你在正确的轨道上。您最终会得到一些丑陋的代码(例如为每个单体类创建头接口),但这应该只是一个中间步骤。

我建议购买一份有效地使用旧代码的副本。首先,您可以从阅读此蒸馏开始。

除了 Karl 的选项(允许您通过拦截进行模拟)之外,您还可以使用Microsoft Fakes & Stubs。但这些工具不会鼓励您重构代码以遵守 SOLID 原则。

于 2013-08-26T18:35:42.740 回答
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

现在,如您所见,这个类容易测试。我们不仅可以轻松创建模拟数据访问对象,而且现在我们可以轻松地将模拟数据访问对象注入到业务对象中。现在我们可以创建一个模拟,它可以快速轻松地准确返回我们希望它返回的数据,然后查看业务类是否返回正确的计算——不涉及数据库。

不幸的是,虽然向现有类添加接口是一件轻而易举的事,但重构它们以使用依赖注入通常需要更多的工作。您可能需要计划首先解决哪些课程最有意义。您可能需要创建一些中间的老式包装器,它们按照过去的代码方式工作,因此在重构代码的过程中不会破坏现有代码。这不是一件快速而容易的事情,但如果你有耐心并且长期坚持下去,是有可能做到的,你会很高兴你做到了。

于 2013-08-26T19:12:56.130 回答
1

我建议您走这interface条路,但如果您想为解决方案付费,请尝试以下方法之一:

于 2013-08-26T18:33:58.917 回答
0

创建接口来测试类并不是一个坏主意——单元测试的目标是测试类上的功能是否按预期运行。根据您正在使用的类,这说起来容易做起来难 - 如果对全局状态等有很多依赖关系,您将需要相应地模拟。

考虑到单元测试的价值,在其中投入一些工作(在一定限度内)将使您和与您一起工作的开发人员受益。

于 2013-08-26T18:34:56.887 回答
0

我更喜欢创建接口和类,因为您需要测试事物,而不是全部预先测试。

除了接口,您还可以使用一些技术来测试遗留代码。我经常使用的是“Extract And Override”,你可以在其他方法中提取一些“不可测试”的代码并使其可覆盖。它们派生您要测试的类,并使用一些感应代码覆盖“不可测试”的方法。

使用模拟框架就像在方法中添加关键字 Overridable 并使用模拟框架设置返回一样简单。

您可以在“有效地使用遗留代码”一书中找到许多技术。

关于现有代码的一件事是,有时编写集成测试比编写单元测试更好。在您测试了行为之后,您可以创建单元测试。

另一个技巧是从依赖较少的模块/类开始,这样,您就可以轻松地熟悉代码。

如果您需要有关“提取和覆盖”的示例,请告诉我;)

于 2013-08-26T18:56:16.923 回答