【问题标题】:Unit Testing concrete classes单元测试具体类
【发布时间】:2013-08-26 18:25:03
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: vb.net unit-testing


    【解决方案1】:

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

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

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

    我敢打赌,如果您希望能够对某些现有类进行单元测试,那么您会发现自己需要重构一些现有的类,以便您可以将高级测试用作 安全网,它会告诉你是否损坏了任何东西。

    【讨论】:

    • +1 表示最后一段。我正在使用测试驱动开发重构代码基础。是否有任何测试工具可以推荐用于集成测试?功能测试还是验收测试?
    • 对于需要查看几个类一起工作的集成测试,您可以使用其中一个 xUnit 库。对于更高级别的测试,它将取决于您的程序的接口:看看 Selenium Webdriver、Fitnesse 之类的东西......
    • 我强烈同意更高级别的测试应该是这里的首要任务。修复错误并能够断言系统正常工作通常比改进代码设计更紧迫。我建议坚持使用 xUnit 框架,并只测试使用该框架可以达到的最高级别。
    【解决方案2】:

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

    我建议投资一份Working Effectively with Legacy Code。首先你可以从阅读this distillation开始。

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

    【讨论】:

      【解决方案3】:

      是的,这是一个好的开始,但是,拥有接口的优先级低于注入依赖项。如果您所有的遗留类都获得了接口,但在内部隐藏了它们仍然是相互依赖的,那么这些类仍然不会更容易测试。例如,假设您有两个如下所示的类:

      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
      

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

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

      【讨论】:

      • 以及全面的答案。
      • 谢谢。如果您能在此处查看我的数据仓库/VB.NET 问题,我将不胜感激:stackoverflow.com/questions/18495622/…。你已经为设计模式类型问题提供了很好的答案,所以我想我会问。
      【解决方案4】:

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

      【讨论】:

      • 付费路线+1。你知道任何可以用来模拟具体类的免费工具吗?我相信我的公司会在需要时付款。
      • 我认为 Microsoft MolesMicrosoft Fakes 会这样做,但不确定。 Microsoft Fakes 也不是真正免费的,因为您必须拥有 Visual Studio 的 Ultimate 版本才能获得它。 Microsoft Moles 可能值得一看。
      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

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

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

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

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

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-09-05
          • 1970-01-01
          • 2010-11-06
          • 2019-02-03
          • 2015-03-16
          • 1970-01-01
          • 2016-12-06
          • 2021-12-23
          相关资源
          最近更新 更多