单元测试Unit Testing [7]

2.1.2 隔离问题:经典方式的观点

  重申一下,伦敦风格的做法是通过测试替身(特别是模拟)的帮助,将被测试的代码片断与它的合作者隔离开来,来达到隔离的要求。有趣的是,这个观点也会影响到你对什么是一小段代码(一个单元)的看法。下面是一个单元测试的所有属性:

  • 一个单元测试验证一小段代码(一个单元)
  • 是否做得很迅速
  • 而且是否以一种孤立的方式进行

  除了第三个属性留有解释的空间外,第一个属性的可能解释也有一些空间。一小段代码应该有多小?正如你在上一节中所看到的,如果你采用隔离每一个单独的类的立场,那么很自然地接受被测试的那段代码也应该是一个单独的类,或者该类中的一个方法。由于你处理隔离问题的方式,它不可能超过这个范围。在某些情况下,你可能会同时测试几个类;但总的来说,你会一直努力保持一次测试一个类的单元测试这一准则。
  正如我前面提到的,还有另一种方式来解释隔离属性–经典的方式。在经典的方法中,不是代码需要以隔离的方式进行测试。 相反,单元测试本身应该在相互隔离的情况下运行。这样一来,你可以平行地、顺序地、按任何顺序地运行测试,只要是最适合你的,它们仍然不会影响彼此的结果。
  互相隔离(Isolating tests)测试意味着只要它们都驻留在内存中,并且不接触到共享状态,就可以同时锻炼几个类,通过共享状态,测试可以互相交流并影响对方的执行环境。这种共享状态的典型例子是进程外的依赖–数据库、文件系统,等等。
  例如,一个测试可以在数据库中创建一个客户,作为其安排阶段的一部分,而另一个测试将删除它,作为其自己安排阶段的一部分,在第一个测试完成执行之前。 如果你平行运行这两个测试,第一个测试会失败,不是因为生产代码被破坏,而是因为第二个测试的干扰。

共享的、私有的和进程外的依赖关系
  共享依赖是指在测试之间共享的依赖,并为这些测试提供影响彼此结果的手段。一个典型的共享依赖的例子是一个静态可变的字段。对这样一个字段的改变在同一进程中运行的所有单元测试中是可见的。数据库是另一个共享依赖的典型例子。
  私有依赖是指不共享的依赖。
  进程外依赖是指在应用程序执行进程之外运行的依赖;它是对尚未进入内存的数据的代理。在绝大多数情况下,进程外依赖与共享依赖相对应,但并非总是如此。例如,一个数据库既是进程外的又是共享的。但如果你在每次测试运行前在Docker容器中启动该数据库,这将使这个依赖成为进程外的,但不是共享的,因为测试不再与它的同一实例一起工作。 同样,一个只读的数据库也是进程外的,但不是共享的,即使它被测试重复使用。测试不能改变这种数据库中的数据,因此不能影响彼此的结果。

  对隔离问题的这种看法,需要对mock和其他测试替身的使用采取更温和的观点。你仍然可以使用它们,但你通常只对那些在测试之间引入共享状态的依赖关系使用。图2.3显示了它的样子。

单元测试Unit Testing [7] 图2.3 单元测试之间的隔离,需要将被测类与共享的依赖关系隔离。私有的依赖关系可以保持不变。

 
  请注意,共享的依赖是在单元测试之间共享,而不是在被测类(单元)之间共享。在这个意义上,只要你能在每个测试中创建一个新的实例,那么单例依赖就不是共享的。虽然在生产代码中只有一个单例的实例,但测试很可能不遵循这个模式,没有重用这个单例。因此,这样的依赖关系是私有的。
  例如,通常只有一个配置类的实例,它在所有生产代码中被重复使用。但是,如果它被注入到SUT中,例如,通过一个构造函数,你可以在每个测试中创建一个新的实例;你不必在整个测试套件中维护一个实例。然而,你不能创建一个新的文件系统或数据库;它们必须在测试之间共享,或用测试替身来替代。

共享与不稳定的依赖关系
  另一个术语也有类似的意思,但不完全相同:易变的依赖性。我推荐Dependency Injection。Steven van Deursen和Mark Seemann的《原则、实践、模式》(Manning Publications,2018年)是一本关于依赖性管理主题的首选书籍。
  一个不稳定的依赖关系是表现出以下特性之一的依赖关系:

  • 除了默认安装在开发者机器上的东西之外,它还引入了设置和配置运行时环境的要求。数据库和API服务就是很好的例子。它们需要额外的设置,而且默认情况下不会安装在你的组织中的机器上。
  • 它包含非确定性的行为。一个例子是一个随机数发生器或一个返回当前日期和时间的类。 这些依赖关系是非确定性的,因为它们在每次调用时提供不同的结果。

  正如你所看到的,共享和易变依赖的概念之间存在重叠。例如,对数据库的依赖既是共享的也是易变的。但文件系统不是这样的。文件系统不是易变的,因为它被安装在每个开发人员的机器上,而且在绝大多数情况下它的行为是确定的。然而,文件系统引入了一种手段,单元测试可以干扰彼此的执行环境;因此它是共享的。同样,随机数发生器是不稳定的,但是因为你可以为每个测试提供一个单独的实例,所以它不是共享的。

  替代共享依赖的另一个原因是提高测试执行速度。共享的依赖关系几乎总是在执行过程之外,而私有的依赖关系通常不跨越这个边界。 正因为如此,对共享依赖的调用,如数据库或文件系统,比对私有依赖的调用需要更多的时间。由于快速运行的必要性是单元测试定义的第二个属性,这种调用将共享依赖的测试从单元测试的域推到了集成测试的域。 我在本章后面会更多地谈及集成测试。
  这种对隔离的另一种看法也导致了对构成单元(一小段代码)的不同看法。一个单元不一定非要局限于一个类。你也可以对一组类进行单元测试,只要它们中没有一个是共享的依赖关系。

上一篇:6:面向对象编程-高级(伴生对象,trait,自身类型,implicit)


下一篇:MySQL:inner join的使用方法