【问题标题】:Unit testing: how to test methods with a lot of underlying objects and business logic单元测试:如何测试具有大量底层对象和业务逻辑的方法
【发布时间】:2012-12-27 00:29:59
【问题描述】:

我对单元测试真的很陌生,虽然我在研究上花费了大量时间,但我无法找到适合我的案例的正确方法。

我的代码库很大(大约 3 年的工作),不幸的是耦合度很高,很难测试,而且从来没有对它进行过单元测试。

例如,当尝试测试集合类ProductCollection,更具体地说,它的bool MoveElementAtIndex(Product productToMove, int newIndex) 时,我遇到了以下问题:

  • 首先我必须初始化这个new ProductCollection()
  • 构造函数初始化另一个手工类:new KeyedList<ID, Product>。我想这不应该在这个构造函数中调用,因为我没有测试KeyedList
  • 接下来,我正在尝试将 3 个产品添加到此 ProductCollection
  • 然后我首先创建这 3 个new Product()
  • 但是Product 类的构造函数做了几件事
  • 它为新创建的产品计算一个唯一 ID:this.ID = IDUtils.ComputeNewIDBasedOnTheMoonPhase()。我想我也不应该测试这个,因为它不是我的范围。我应该如何避免这种深度的调用?
  • 相同的 Product 构造函数为此产品分配了一些默认属性:this.Properties = new ProductProperties(folderPathToDefaultProperties)。这不应该从我简单的FieldCollection.MoveElementAtIndex 测试中调用,对吧?
  • 假设我现在终于有了我的产品对象,我正在尝试将它们添加到我的收藏中。
  • 但是ProductCollection.Add(MyProduct) 检查底层KeyedList 是否已经包含该产品。这也是我应该避免的业务逻辑,与我的测试无关。问题是如何?
  • 此外,在这个Add 方法中,会引发一些事件,通知系统一些事情(例如,新产品已添加到集合中)。我想这些也不应该被解雇。
  • 最后,当我添加我的产品时,我将调用所需的 SUT:移动元素方法。
  • 但是这个方法也有可能超出我的测试范围的逻辑:它验证底层KeyedList实际上包含这些字段,它调用KeyedList.Remove()KeyedList.Insert()的移动逻辑,它触发事件喜欢CollectionModified

如果您能解释一下如何正确地进行此单元测试,如何避免调用底层对象,我将不胜感激。

我正在考虑微软的 Moles 框架 (VS2010),因为我的印象是它不需要我重构所有东西,因为这绝对不是一个选择。但是已经试过了,还是找不到合适的使用方法。

另外,我的印象是,这个具体的例子会在我的情况下帮助很多人,因为现实世界中的代码通常是这样的。

有什么想法吗?

【问题讨论】:

    标签: c# unit-testing mocking moles coupling


    【解决方案1】:

    这是一个常见问题,也是我们有框架为我们伪造对象的原因。存根或模拟允许我们围绕一个类创建测试工具,而无需实例化许多其他类和服务。有许多这样的框架。这是一个非常有用的三个常用框架的博客。 http://www.richard-banks.org/2010/07/mocking-comparison-part-1-basics.html

    我自己对单元测试很陌生,发现 Roy Osherove 的《单元测试的艺术》一书非常有帮助。 Roy 有一个博客,这些是他关于单元测试的一些帖子http://osherove.com/display/Search?moduleId=10002929&searchQuery=Unit+Testing

    需要考虑的另一件事是您的类的耦合程度。做起来可能不是那么容易,但你可能想看看依赖注入。这允许类更松散耦合,因此更容易测试。我发现 Mark Seemann 的 .NET 中的 Dependency Injection 一书是一个很好的介绍。

    在我的研究之后,我选择NUnit 作为测试框架,NSubstitute 用于模拟和存根,Fluent Assertions 使编写测试更容易,NInject 用于依赖注入

    【讨论】:

      【解决方案2】:

      我建议使用ApprovalTest。这是开始测试没有最佳设计的遗留系统的好工具。

      现在不要打扰单元测试和集成测试的区别,也不要打扰完全隔离你的类。您的代码可能不是最适合可测试性的,当您开始将所有内容相互隔离时 - 您最终会得到巨大的排列部分和非常脆弱的测试。

      另一方面,您必须隔离外部资源(Web 服务、数据库、文件系统等)。此外,应隔离所有非确定性行为(Random、当前时间、用户输入等)

      我只是建议创建一个验证测试的安全网,这将帮助您朝着可测试性的方向更改软件,并告诉您是否对代码进行了重大更改。

      阅读 Michael Feathers Working Effectively with Legacy Code

      【讨论】:

        【解决方案3】:

        您的代码在设计时并未考虑到单元测试,因此很难做到这一点。我建议您正确设计和单元测试您的新代码,并尝试重构最重要的东西,以便您可以对其进行单元测试。

        例子:

        但是 Product 类的构造函数做了几件事。 它为新创建的产品计算一个唯一 ID:this.ID = IDUtils.ComputeNewIDBasedOnTheMoonPhase()。我想我不应该测试 这也不是,因为这不是我的范围。我应该如何避免这样的电话 到这种深度?

        要解决这个问题,您应该将接口 IUtils 传递给您的 Product 构造函数。要测试您的 Product 类,您可以创建一个返回设定值的 IUtils 模拟。您可以对 ProductProperties 执行相同的操作。

        另外,在这个 Add 方法中,会引发一些事件,通知系统 关于几件事(例如,将新产品添加到 收藏)。我想这些也不应该被解雇。

        这归结为设计。您可以在单元测试时使用观察者模式并且没有任何观察者。

        【讨论】:

          【解决方案4】:

          您应该使用 microsoft moles 框架或 microsoft fakes 框架。这些框架使您能够更改函数的行为。

          在适当的单元测试中,所有外部方法调用都应被模拟以隔离要测试的代码。 Moles / Fakes 在创建模拟对象 / 存根等方面非常有用。

          更新: 从理论上讲,最好模拟所有外部方法调用以隔离代码。即使在某些实际情况下,您也必须模拟所有外部方法:

          void UpdateSomeX(X x)
          {
             this.validator.Validate(x);
             x.UpdateDate = DateTime.Now;
             this.context.Attach(x);
             this.unitOfWork.Save();
          }
          

          猜猜看;您最终将模拟所有外部方法调用。那你需要测试这个方法吗?答案是肯定的,但是“是”的细节超出了这个问题的范围。

          【讨论】:

          • 你的意思可能是stubbedmartinfowler.com/articles/mocksArentStubs.html 而且我也不太了解所有外部方法。你会存根Math.Abs 吗?
          • 你会存根 Datetime.Now 吗?
          • 并非总是如此。如果它不受执行流程的控制并且没有被断言。无论如何,声明是存根 all 外部方法。对了,DateTime.Now应该是隔离注入的。就像 Jon Skeet 在 NodeTime 中通过注入 IClock 接口所做的那样。所以你会存根时间提供者,而不是静态调用。
          • @IlyaIvanov DateTime.Now 不一定要隔离注入;事实上,moles 框架的第一个例子是如何模拟 datetime.now 。我不再继续讨论了,这对我们双方都没有帮助。
          猜你喜欢
          • 2012-02-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-08
          • 2019-07-09
          • 1970-01-01
          • 2021-06-16
          • 1970-01-01
          相关资源
          最近更新 更多