单元测试具体类

时间:2013-08-26 18:25:03

标签: vb.net unit-testing

我继承了一个没有接口或抽象类的项目,即仅具体类,我想引入单元测试。这些类包含许多函数,包含业务逻辑和数据逻辑;打破每个SOLID规则(http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29)。

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

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

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

6 个答案:

答案 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)

我建议您转到interface路线,但如果您想支付解决方案,请尝试以下方法之一:

答案 4 :(得分:0)

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

考虑到有价值的单元测试,将一些工作放入其中(达到极限)将使您和与之合作的开发人员受益。

答案 5 :(得分:0)

我更喜欢创建接口和类,因为您需要测试的东西而不是所有的东西。

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

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

您可以在“Working Effectively with Legacy Code”一书中找到许多技巧。

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

另一个提示是从具有较少依赖性的模块/类开始,这样,您就可以轻松熟悉代码。

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