Chapter 2. What is a unit test?

本章涵盖什么是单元测试共享,私有和易失性依赖项之间的区别单元测试的两个流派:古典和伦敦单元测试,集成测试和端到端测试之间的差异

如第一章所述,在单元测试的定义中有许多令人惊讶的细微差别。 这些细微差别比您想象的要重要得多,以至于在解释它们时的差异导致了关于如何进行单元测试的两种不同观点。

这些观点被称为经典和伦敦的单元测试流派。 古典流派被称为“古典流派”,因为它是每个人最初采用单元测试和测试驱动开发的方式。 伦敦学校扎根于伦敦的编程社区。 本章中有关古典风格和伦敦风格之间的差异的讨论奠定了第5章的基础,在第5章中,我详细介绍了模拟和测试脆弱性这一主题。

首先,定义一个单元测试,其中包含所有应注意的事项和细微之处。 这个定义是区分古典学校和伦敦学校的关键。

2.1 “单元测试”的定义

单元测试有很多定义。 除去它们的不必要的位,这些定义都具有以下三个最重要的属性。 单元测试是一种自动化测试,

  • 验证一小段代码(也称为单元),

  • 快点

  • 并以孤立的方式进行。

这里的前两个属性毫无争议。 关于什么真正构成快速的单元测试可能存在一些争议,因为这是一种高度主观的措施。 但总的来说,它并不那么重要。 如果测试套件的执行时间对您来说足够好,则意味着您的测试足够快。

人们有很大不同的看法是第三个属性。 隔离问题是古典和伦敦单元测试学校之间差异的根源。 正如您将在下一节中看到的那样,这两种流派之间的所有其他差异自然都源于对隔离意味着什么的单一分歧。 我更喜欢古典风格,因为我会在2.3节中进行描述。

单元测试的经典和伦敦学校经典方法也称为底特律,有时也称为经典单元测试方法。 关于古典学派的最经典的书也许是肯特·贝克(Kent Beck)的著作:《测试驱动的发展:以身作则》(Addison-Wesley Professional,2002)。伦敦风格有时被称为嘲讽派。 尽管“嘲笑者”一词很普遍,但是坚持这种单元测试风格的人们通常不喜欢它,因此在本书中,我将其称为伦敦风格。 这种方法最著名的支持者是史蒂夫·弗里曼和纳特·普赖斯。 我推荐他们的书《成长的面向对象软件》(由测试指导)(Addison-Wesley Professional,2009年),作为这方面的很好资料。

2.1.1 隔离问题:伦敦之旅

以隔离的方式验证一段代码(一个单元)是什么意思? 伦敦的学校将其描述为将测试中的系统与合作者隔离开来。 这意味着,如果一个类对另一个或多个类具有依赖关系,则需要用测试双精度替换所有此类依赖关系。 这样,您可以通过将其行为与任何外部影响分开来专门关注被测类。

定义双重测试是一个对象,其外观和行为类似于预期的发布对象,但实际上是简化版本,可以降低复杂性并简化测试。 这个术语是Gerard Meszaros在他的《 xUnit测试模式:重构测试代码》(Addison-Wesley,2007年)中引入的。 名称本身来自电影中特技替身的概念。

图2.1显示了通常如何实现隔离。 单元测试可以以其他方式验证被测系统及其所有依赖关系,现在可以与那些依赖关系分开进行。


这种方法的一个好处是,如果测试失败,则您可以确定知道代码库的哪一部分被破坏了:这就是被测试的系统。 不可能有其他嫌疑犯,因为班上的所有邻居都被双打考试取代。

另一个好处是能够拆分对象图-沟通类网络解决了同一问题。 该网络可能会变得非常复杂:其中的每个类都可能具有多个直接依赖项,每个依赖项都依赖于它们自己的依赖项,依此类推。 类甚至可能引入循环依赖关系,其中依赖关系链最终会回到其起点。

没有双倍的测试,很难测试这种互连的代码库。 剩下的唯一选择几乎是在测试中重新创建完整的对象图,如果其中的类数太多,这可能不是一个可行的任务。

使用测试双打,您可以停止这一步。 您可以替换类的直接依赖关系。 而且,通过扩展,您不必处理这些依赖项的依赖项,依此类推。 您正在有效地分解图表,这可以大大减少您在单元测试中要做的准备工作量。

而且,别忘了这种用于单元测试隔离的方法的另一个小而令人愉快的好处:它允许您引入整个项目范围的准则,一次只能测试一个类,从而在整个单元测试套件中建立了一个简单的结构。 您不再需要考虑如何用测试覆盖代码库。 有课吗 用单元测试创​​建一个相应的类! 图2.2显示了它通常的外观。

假设我们经营一家在线商店。 我们的示例应用程序中只有一个简单的用例:客户可以购买产品。 如果商店中有足够的库存,则认为购买成功,并且商店中的产品数量将减少购买数量。 如果产品不足,则购买将失败,商店中也不会发生任何事情。

清单2.1显示了两个测试,验证只有在商店中有足够的库存时,购买才能成功。 测试以古典风格编写,并使用典型的三相顺序:排列,动作和声明(简称AAA,我将在第3章中详细介绍此顺序)。

[Fact]
public void Purchase_succeeds_when_enough_inventory()
{// Arrangevar store = new Store();store.AddInventory(Product.Shampoo, 10);var customer = new Customer();// Actbool success = customer.Purchase(store, Product.Shampoo, 5);// AssertAssert.True(success);Assert.Equal(5, store.GetInventory(Product.Shampoo));
}[Fact]
public void Purchase_fails_when_not_enough_inventory()
{// Arrangevar store = new Store();store.AddInventory(Product.Shampoo, 10);var customer = new Customer();// Actbool success = customer.Purchase(store, Product.Shampoo, 15);// AssertAssert.False(success);Assert.Equal(10, store.GetInventory(Product.Shampoo));
}public enum Product
{Shampoo,Book
}

如您所见,安排部分是测试准备好所有依赖关系和被测试系统的地方。 对customer.Purchase()的调用是行为阶段,您在其中执行要验证的行为。 assert语句是验证阶段,您可以在其中检查行为是否导致了预期的结果。

在安排阶段,测试将两种对象放在一起:被测系统(SUT)和一个协作者。 在这种情况下,客户是SUT,商店是合作者。 我们需要合作者有两个原因:

  • 要编译要测试的方法,因为customer.Purchase()需要一个Store实例作为参数

  • 对于断言阶段,由于customer.Purchase()的结果之一是商店中产品数量的潜在减少

Product.Shampoo以及数字5和15是常数。

定义被测方法(MUT)是被测程序在SUT中调用的方法。 术语MUT和SUT通常用作同义词,但通常,MUT指的是一种方法,而SUT指的是整个类。

这段代码是经典的单元测试样式的示例:该测试不会替代协作者(Store类),而是使用可用于生产的实例。 这种风格的自然结果之一就是该测试现在可以有效地验证客户和商店,而不仅仅是客户。 Store的内部运作中任何会影响客户的错误都会导致这些单元测试失败,即使客户仍然可以正常工作。 在测试中,这两个类不是相互隔离的。

现在,我们将示例修改为伦敦风格。 我将进行相同的测试,并将商店实例替换为测试双打(特别是模拟)。

我使用Moq(https://github.com/moq/moq4)作为模拟框架,但是您可以找到几种同样不错的替代方案,例如NSubstitute(https://github.com/nsubstitute/NSubstitute)。 所有面向对象的语言都有类似的框架。 例如,在Java世界中,您可以使用Mockito,JMock或EasyMock。

定义模拟是一种特殊的双重测试,可让您检查被测系统及其协作者之间的交互。

在后面的章节中,我们将回到模拟,存根及其之间的差异这一主题。 现在,要记住的主要事情是模拟是测试倍数的子集。 人们经常使用术语test double和嘲笑作为同义词,但是从技术上讲,它们不是(第5章中对此有更多介绍):

  • Test double是一个总体术语,它描述了测试中各种非生产就绪的虚假依赖关系。

  • 模拟只是这种依赖的一种。

下一个清单显示了将Customer与合作者Store隔离后的测试结果。

[Fact]
public void Purchase_succeeds_when_enough_inventory()
{// Arrangevar storeMock = new Mock<IStore>();storeMock.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)).Returns(true);var customer = new Customer();// Actbool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);// AssertAssert.True(success);storeMock.Verify(x => x.RemoveInventory(Product.Shampoo, 5),Times.Once);
}[Fact]
public void Purchase_fails_when_not_enough_inventory()
{// Arrangevar storeMock = new Mock<IStore>();storeMock.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)).Returns(false);var customer = new Customer();// Actbool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);// AssertAssert.False(success);storeMock.Verify(x => x.RemoveInventory(Product.Shampoo, 5),Times.Never);
}

请注意,这些测试与古典风格的测试有何不同。 在安排阶段,测试不再使用Moq的内置类Mock 实例化Store的生产就绪实例,而是为其创建替代实例。

此外,我们没有通过向其添加洗发水库存来修改Store的状态,而是直接告诉该模型如何响应对HasEnoughInventory()的调用。 无论存储的实际状态如何,模拟都会以测试所需的方式对此请求做出响应。 实际上,测试不再使用Store-我们引入了IStore接口,并且正在模拟该接口而不是Store类。

在第8章中,我详细介绍了如何使用接口。 现在,仅需注意,需要有接口才能将被测系统与其协作者隔离开来。 (您也可以模拟一个具体的类,但这是一种反模式;我将在第11章中介绍该主题。)

断言阶段也发生了变化,这就是关键的区别所在。 我们仍然检查customer。输出的输出,就像以前一样,但是验证客户对商店执行正确操作的方式有所不同。 以前,我们是通过断言商店的状态来做到这一点的。 现在,我们检查“客户”与“商店”之间的交互:测试将检查客户是否在商店上进行了正确的呼叫。 我们通过传递客户应在商店上调用的方法(x.RemoveInventory)以及执行此操作的次数来做到这一点。 如果购买成功,则客户应调用一次此方法(Times.Once)。 如果购买失败,则客户根本不应致电(Times.Never)。

2.1.2 隔离问题:经典之作

重申一下,London风格通过在测试倍数的帮助下将测试中的代码段与其协作者隔离开来满足隔离要求:特别是模拟。 有趣的是,这种观点也会影响您对构成一小段代码(一个单元)的观点。 再次是单元测试的所有属性:

  • 单元测试验证一小段代码(一个单元),

  • 快点

  • 并以孤立的方式进行。

除了第三个属性留下解释的空间外,第一个属性的可能解释也有一些余地。 一小段代码应该有多小? 从上一节中可以看到,如果您采用隔离每个类的立场,那么很自然地接受测试的代码段也应该是单个类,或者是该类中的一个方法。 由于您处理隔离问题的方式,不能超过此数目。 在某些情况下,您可能会同时测试几个课程; 但总的来说,您将始终努力保持每次单元测试一个单元的指导原则。

正如我之前提到的,还有另一种解释隔离属性的方法-经典方法。 在传统方法中,不需要单独测试代码。 相反,单元测试本身应该彼此隔离地运行。 这样,您可以以最合适的方式并行,顺序和以任何顺序运行测试,而它们仍然不会影响彼此的结果。

相互隔离测试意味着可以一次练习多个类,只要它们都驻留在内存中并且不会达到共享状态即可,测试可以通过这些状态相互交流并影响彼此的执行上下文。 这种共享状态的典型示例是进程外依赖性(数据库,文件系统等)。

例如,在第一个测试完成执行之前,一个测试可以在数据库的安排阶段中创建一个客户,而另一个测试将在其自己的安排阶段中将其删除。 如果并行运行这两个测试,则第一个测试将失败,这不是因为生产代码已损坏,而是由于第二个测试的干扰。

共享,私有和进程外依赖性

共享依存关系是测试之间共享的依存关系,并为这些测试提供了相互影响结果的手段。 共享依赖项的一个典型示例是静态可变字段。 在同一流程中运行的所有单元测试中,都可以看到对该字段的更改。 数据库是共享依赖项的另一个典型示例。

私有依存关系是未共享的依存关系。

进程外依赖关系是在应用程序执行过程之外运行的依赖关系; 它是尚未存储在内存中的数据的代理。 在大多数情况下,进程外依赖项对应于共享的依赖项,但并非总是如此。 例如,数据库是进程外的和共享的。 但是,如果您在每次测试运行之前在Docker容器中启动该数据库,这将使该依赖项脱离进程,但不会共享,因为测试不再使用该实例的同一实例。 同样,即使测试将其重用,只读数据库也将处于进程外,但不会共享。 测试无法使此类数据库中的数据发生变异,因此不会影响彼此的结果。

这种隔离问题需要对模拟和其他测试双精度的使用更为适度的看法。 您仍然可以使用它们,但是通常只对那些在测试之间引入共享状态的依赖项执行此操作。 图2.3显示了外观。

请注意,共享的依赖关系在单元测试之间共享,而不是在被测类(单元)之间共享。 从这种意义上说,只要您能够在每个测试中创建新的实例,就不会共享单例依赖。 虽然生产代码中只有一个单例实例,但是测试很可能不会遵循这种模式,也不会重用该单例。 因此,这种依赖性将是私有的。

例如,通常只有一个配置类的实例,该实例可在所有生产代码中重复使用。 但是,如果以其他所有依赖项的方式(例如通过构造函数)将其注入到SUT中,则可以在每次测试中为其创建一个新实例; 您不必在整个测试套件中维护一个实例。 但是,您无法创建新的文件系统或数据库。 它们必须在测试之间共享或用测试倍数代替。

共享与易失性依赖关系

另一个术语具有相似但不相同的含义:可变依赖项。 我建议由Steven van Deursen和Mark Seemann(Manning Publications,2018)撰写的依赖注入:原理,实践,模式(Manning Publications,2018)作为有关依赖管理主题的书籍。

易失性依存关系是具有以下属性之一的依存关系:

  • 除了默认情况下,它还要求设置和配置运行时环境,以及在开发人员计算机上安装的环境。 数据库和API服务是这里的很好的例子。 它们需要其他设置,并且默认情况下未安装在组织中的计算机上。

  • 它包含不确定的行为。 一个示例是随机数生成器或返回当前日期和时间的类。 这些依赖性是不确定的,因为它们在每次调用时提供不同的结果。

如您所见,共享依赖项和易失性依赖项之间存在重叠。 例如,对数据库的依赖既是共享的又是易失的。 但这不是文件系统的情况。 文件系统不是易失的,因为它已安装在每个开发人员的计算机上,并且在大多数情况下都具有确定性。 尽管如此,文件系统还是引入了一种方法,单元测试可以通过这种方法来干扰彼此的执行上下文。 因此,它是共享的。 同样,随机数生成器是易失的,但是由于您可以为每个测试提供单独的实例,因此不会共享。

替换共享依赖项的另一个原因是提高测试执行速度。 共享的依赖关系几乎总是驻留在执行过程之外,而私有的依赖关系通常不会越过边界。 因此,对共享依赖项(例如数据库或文件系统)的调用要比对私有依赖项的调用花费更多的时间。 而且由于必须快速运行是单元测试定义的第二个属性,因此此类调用将具有共享依赖项的测试从单元测试的领域推到了集成测试的领域。 在本章稍后,我将进一步讨论集成测试。

这种隔离的替代观点也导致对构成单元(一小段代码)的构成有不同的看法。 一个单元并不一定仅限于一个类别。 您可以对一组类进行单元测试,只要它们都不是共享依赖项即可。

2.2 古典和伦敦单元测试学校

如您所见,伦敦学校和古典学校之间差异的根源是隔离属性。 伦敦的学校将其视为被测试系统与合作者之间的隔离,而经典的学校则将其视为单元测试彼此之间的隔离。

这种看似微小的差异导致了关于如何进行单元测试的巨大分歧,正如您已经知道的那样,这产生了两种思想流派。 总体而言,学校之间的分歧涵盖三个主要主题:

  • 隔离要求

  • 构成测试代码的部分(单元)

  • 处理依赖

表2.1总结了所有内容。

2.2.1 古典学校和伦敦学校如何处理依附关系

请注意,尽管普遍使用了测试倍数,但伦敦学校仍然允许在测试中按原样使用某些依赖项。 这里的试金石测试是依赖项是否可变。 最好不要替换永远不变的对象-不可变的对象。

在前面的示例中,您看到了当我将测试重构为伦敦风格时,我没有用模拟代替Product实例,而是使用了真实的对象,如以下代码所示(为方便起见,从清单2.2重复进行了说明) ):

[Fact]
public void Purchase_fails_when_not_enough_inventory()
{// Arrangevar storeMock = new Mock<IStore>();storeMock.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5)).Returns(false);var customer = new Customer();// Actbool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);// AssertAssert.False(success);storeMock.Verify(x => x.RemoveInventory(Product.Shampoo, 5),Times.Never);
}

在Customer的两个依赖项中,只有Store包含可以随时间变化的内部状态。 产品实例是不可变的(产品本身是C#枚举)。 因此,我仅替换了Store实例。

如果您考虑一下,这是有道理的。 您也不会在上一个测试中对5号使用双精度测试,对吗? 那是因为它也是不可变的-您无法修改此数字。 请注意,我并不是在讲包含数字的变量,而是数字本身。 在语句RemoveInventory(Product.Shampoo,5)中,我们甚至没有使用变量; 立即声明5。 Product.Shampoo也是如此。

这种不可变的对象称为值对象或值。 他们的主要特征是他们没有个人身份。 它们仅由其内容标识。 因此,如果两个这样的对象具有相同的内容,则使用哪个对象都没有关系:这些实例是可互换的。 例如,如果您有两个5个整数,则可以用它们代替一个整数。 在我们的案例中,产品也是如此:您可以重复使用一个Product.Shampoo实例或声明其中的多个实例-这不会有任何区别。 这些实例将具有相同的内容,因此可以互换使用。

请注意,值对象的概念与语言无关,不需要特定的编程语言或框架。 您可以在http://mng.bz/KE9O上的文章“实体与价值对象:差异的终极清单”中了解有关价值对象的更多信息。

图2.4显示了依赖项的分类以及两个单元测试流派如何对待它们。 依赖关系可以是共享的也可以是私有的。 反过来,私有依赖关系可以是可变的或不可变的。 在后一种情况下,它称为值对象。 例如,数据库是一个共享的依赖项,它的内部状态在所有自动化测试中共享(不将其替换为测试重复数)。 Store实例是可变的私有依赖项。 一个Product实例(或者说是5的实例)是一个不可变的私有依赖项的例子-一个值对象。 所有共享的依赖项都是可变的,但是要共享可变的依赖项,必须将其重新用于测试。

为了方便起见,我在表2.1中重复了学校之间的区别。

协作者与依存关系

协作者是共享的或可变的依赖项。 例如,由于数据库是共享依赖项,因此提供对数据库访问权限的类是协作者。 Store也是合作者,因为它的状态会随着时间而改变。

产品和5也是依赖项,但它们不是合作者。 它们是价值或价值对象。

一个典型的类可以使用两种类型的依赖项:协作者和值。 查看此方法调用:

customer.Purchase(store, Product.Shampoo, 5)

在这里,我们有三个依赖项。 其中一个(商店)是协作者,而其他两个(Product.Shampoo,5)则不是协作者。

让我重申一点关于依赖关系的类型。 并非所有进程外依赖项都属于共享依赖项类别。 共享的依赖关系几乎总是驻留在应用程序的流程之外,但事实并非如此(见图2.5)。 为了共享进程外依赖性,它必须提供单元测试相互通信的方法。 通过修改依赖项的内部状态来完成通信。 从这个意义上讲,不变的进程外依赖性并不提供这种方法。 测试根本无法修改其中的任何内容,因此不会干扰彼此的执行上下文。

例如,如果某处有一个API返回组织出售的所有产品的目录,则这不是共享依赖项,只要该API不公开更改目录的功能即可。 确实,这种依赖关系是易变的,位于应用程序的边界之外,但是由于测试不会影响它返回的数据,因此不会共享它。 这并不意味着您必须在测试范围内包括这种依赖性。 在大多数情况下,您仍然需要将其替换为测试倍数以保持测试快速。 但是,如果进程外依赖关系足够快并且与之的连接稳定,那么可以在测试中按原样使用它。

话虽如此,除非我另有明确说明,否则在本书中,我将术语共享依赖和进程外依赖互换使用。 在现实世界中的项目中,您很少会遇到共享的依赖关系,并且这种共享的依赖关系不会失调。 如果某个依赖项正在处理中,则可以轻松地为每个测试提供一个单独的实例; 无需在测试之间共享它。 同样,您通常不会遇到未共享的进程外依赖关系。 大多数此类依赖项是可变的,因此可以通过测试进行修改。

在定义的基础上,让我们对比一下这两种流派的优点。

2.3 对比经典和伦敦的单元测试流派

重申一下,古典学校和伦敦学校之间的主要区别在于在单元测试的定义中它们如何处理隔离问题。 反过来,这会蔓延到单元的处理(应该测试的东西)以及处理依赖项的方法。

如前所述,我更喜欢经典的单元测试流派。 它倾向于产生更高质量的测试,因此更适合实现单元测试的最终目标,这是项目的可持续发展。 原因是脆弱性:使用模拟的测试比传统的测试更加脆弱(第5章对此进行了详细介绍)。 现在,让我们来看看伦敦学校的主要卖点,并逐一评估它们。

伦敦学校的方法具有以下优点:

  • 更好的粒度。 测试的粒度很细,一次只能检查一门课。

  • 对较大的互连类图进行单元测试更加容易。 由于所有协作者都已被双打考试取代,因此您在编写测试时无需担心。

  • 如果测试失败,则可以确定哪些功能失败。 没有班级的合作者,除了被测班级本身之外,没有其他嫌疑犯。 当然,在某些情况下,被测系统会使用值对象,而此值对象中的更改导致测试失败。 但是这些情况并不常见,因为在测试中消除了所有其他依赖性。

2.3.1 一次单元测试一堂课

关于更好的粒度的观点涉及到关于在单元测试中什么构成单元的讨论。 伦敦学校认为班级就是这样的单元。 来自面向对象的编程背景,开发人员通常将类视为构成每个代码库基础的原子构造块。 这自然也导致将类视为要在测试中验证的原子单位。 这种趋势是可以理解的,但却具有误导性。

小费测试不应验证代码单元。 相反,他们应该验证行为单位:对于问题领域有意义的事物,理想情况下,是业务人员可以识别为有用的事物。 实现这种行为单位所需的类数无关紧要。 该单元可以跨越多个类或仅一个类,甚至仅占用很小的方法。

因此,以更好的代码粒度为目标并没有帮助。 只要测试检查一个行为单元,那就是一个很好的测试。 实际上,将目标对准小于此目标的目标可能会损坏您的单元测试,因为很难确切地了解这些测试所验证的内容。 测试应该讲述一个有关您的代码有助于解决的问题的故事,并且该故事对于非程序员应具有凝聚力和意义。

例如,这是一个连贯故事的示例:

When I call my dog, he comes right to me.

现在将其与以下内容进行比较:

When I call my dog, he moves his front left leg first, then the front right
leg, his head turns, the tail start wagging...

第二个故事意义不大。 这些运动的目的是什么? 狗来找我吗? 还是他逃跑了? 你不知道 当您针对单个班级(狗的腿,头和尾巴)而不是实际行为(狗来了它的主人)时,这就是您的测试开始的样子。 我将在第5章中更多地讨论可观察到的行为这一主题以及如何将其与内部实现细节区分开。

2.3.2 单元测试一个大型的相互连接的类图

使用模拟代替真正的协作者可以更轻松地测试类-尤其是在存在复杂的依赖关系图时,被测类具有依赖关系,每个依赖关系都依赖于其自身的依赖关系,依此类推 深。 使用测试双打,您可以替换类的直接依赖关系,从而破坏图表,这可以大大减少您在单元测试中要做的准备工作量。 如果您遵循古典学派,则仅为了设置被测系统就必须重新创建完整的对象图(共享依赖项除外),这可能需要大量工作。

尽管这都是事实,但这一推理路线着重于错误的问题。 而不是寻找测试互连类的大型复杂图的方法,您应该首先专注于不具有此类的图。 大型类图通常是代码设计问题的结果。

测试指出了这个问题实际上是一件好事。 正如我们在第1章中讨论的那样,对一段代码进行单元测试的能力是一个很好的否定指标-它以相对较高的精度预测较差的代码质量。 如果您发现要对课程进行单元测试,则需要将测试的安排阶段扩展到所有合理的范围之外,这一定有麻烦。 使用模拟只会掩盖这个问题。 它不能解决根本原因。 我将在第2部分中讨论如何解决基础代码设计问题。

2.3.3 显示确切的错误位置

如果将错误引入带有伦敦式测试的系统,通常只会导致SUT包含该错误的测试失败。 但是,采用经典方法时,针对故障类别的客户的测试也可能失败。 这会导致连锁反应,其中单个错误会导致整个系统的测试失败。 结果,很难找到问题的根源。 您可能需要花一些时间调试测试才能弄清楚。

这是一个有效的担忧,但我认为这不是一个大问题。 如果您定期进行测试(最好是在每次源代码更改后进行测试),那么您就会知道导致错误的原因-这是您上一次编辑的错误,因此发现问题并不难。 另外,您不必查看所有失败的测试。 修复一个会自动修复所有其他。

此外,在整个测试套件中级联的故障具有一定的价值。 如果一个错误不仅导致一个测试而且导致很多测试都出错,则表明您刚刚破坏的那段代码非常有价值,整个系统都依赖于此。 在使用代码时,请记住这些有用的信息。

2.3.4 古典学校与伦敦学校之间的其他差异

古典学校和伦敦学校之间剩下的两个区别是

  • 他们通过测试驱动开发(TDD)进行系统设计的方法

  • 规格超标的问题

测试驱动的开发

测试驱动的开发是一种依靠测试来驱动项目开发的软件开发过程。 该过程包括三个阶段(有些作者指定了四个阶段),您需要为每个测试用例重复此阶段:

  • 编写失败的测试以指示需要添加哪些功能以及其行为方式。

  • 编写足够的代码以使测试通过。 在此阶段,代码不必太过优雅或简洁。

  • 重构代码。 在通过测试的保护下,您可以安全地清理代码以使其更具可读性和可维护性。

关于这个主题的好资料是我之前推荐的两本书:Kent Beck的“测试驱动的开发:通过示例”和由Steve Freeman和Nat Pryce的测试指导的“不断增长的面向对象的软件”。

伦敦的单元测试风格导致了由内而外的TDD,您可以从对整个系统设定期望的更高级别的测试开始。 通过使用模拟,您可以指定系统应与哪些协作者进行通信以实现预期的结果。 然后,您将遍历类图,直到实现每个类。 嘲弄使这种设计过程成为可能,因为您一次可以专注于一堂课。 您可以在测试时切断SUT的所有协作者,从而将这些协作者的执行推迟到以后。

古典学校提供的指导不太一样,因为您必须处理测试中的真实对象。 相反,您通常使用由内而外的方法。 在这种样式中,您从域模型开始,然后在其之上放置其他层,直到最终用户可以使用该软件为止。

但是,学校之间最关键的区别是规格过高的问题:即将测试与SUT的实施细节结合起来。 与经典样式相比,伦敦样式往往会产生更多与实现耦合的测试。 这是反对普遍使用嘲笑和伦敦风格的主要反对意见。

关于模拟的话题还有更多。 从第4章开始,我逐步介绍与之相关的所有内容。

2.4 两所学校的融合测试

伦敦学校和古典学校在整合测验的定义上也存在分歧。 这种分歧自然源于他们对隔离问题的看法分歧。

伦敦的学校将任何使用真实协作对象的测试视为集成测试。 伦敦学校的支持者将大多数古典风格的测试视为综合测试。 例如,请参见清单1.4,其中我首先介绍了涉及客户购买功能的两个测试。 从经典的角度来看,该代码是典型的单元测试,但对于伦敦学校的追随者来说,它是一个集成测试。

在本书中,我使用了单元测试和集成测试的经典定义。 同样,单元测试是具有以下特征的自动化测试:

  • 它验证一小段代码,

  • 快点

  • 并以孤立的方式进行。

现在,我已经阐明了第一和第三属性的含义,我将从古典学派的角度重新定义它们。 单元测试是指

  • 验证行为的单个单位,

  • 快点

  • 并将其与其他测试隔离开来。

因此,集成测试是不满足以下条件之一的测试。 例如,无法达到共享依赖关系的测试(例如数据库)不能与其他测试隔离运行。 如果一项测试导致数据库状态发生变化,那么如果并行运行,则依赖于同一数据库的所有其他测试的结果都会发生变化。 您必须采取其他措施来避免这种干扰。 特别是,您必须顺序运行此类测试,以便每个测试都将等待轮流使用共享依赖项。

同样,扩展到进程外依赖项会使测试变慢。 对数据库的调用会增加数百毫秒(可能长达一秒)的额外执行时间。 乍一看,毫秒似乎没什么大不了的,但是当您的测试套件变得足够大时,每一秒都很重要。

从理论上讲,您可以编写一个仅适用于内存中对象的慢速测试,但这并不容易。 同一内存空间内的对象之间的通信比单独的进程之间的通信便宜得多。 即使测试可以处理数百个内存对象,与对象的通信仍然比对数据库的调用执行得更快。

最后,测试是验证两个或更多行为单位的集成测试。 这通常是尝试优化测试套件的执行速度的结果。 当您有两个遵循相似步骤但要验证行为不同单位的慢速测试时,将它们合并为一个可能是有意义的:一个检查两个相似事物的测试比两个粒度更大的测试运行得更快。 但是话又说回来,这两个原始测试本来应该是集成测试(由于它们运行缓慢),因此此特性通常不是决定性的。

集成测试还可以验证由独立团队开发的两个或多个模块如何协同工作。 这也属于第三个测试桶,它们可以一次验证多个行为单位。 但是,由于这种集成通常需要过程外的依赖,因此测试将无法满足所有三个条件,而不仅仅是一个。

集成测试通过验证整个系统在提高软件质量中起着重要作用。 我将在第3部分中详细介绍集成测试。

2.4.1 端到端测试是集成测试的子集

简而言之,集成测试是一种验证您的代码是否与共享依赖项,进程外依赖项或组织中其他团队开发的代码结合使用的测试。 端到端测试还有一个单独的概念。 端到端测试是集成测试的子集。 他们也会检查代码如何与进程外依赖项一起工作。 端到端测试和集成测试之间的区别在于,端到端测试通常包含更多此类依赖项。

有时这条线是模糊的,但是总的来说,集成测试仅对一个或两个进程外依赖项起作用。 另一方面,端到端测试可以处理所有进程外依赖项,也可以处理绝大多数依赖项。 因此,使用端对端的名称,这意味着测试将从最终用户的角度验证系统,包括该系统集成的所有外部应用程序(见图2.6)

人们还使用诸如UI测试(UI代表用户界面),GUI测试(GUI是图形用户界面)和功能测试之类的术语。 术语定义不明确,但是通常,这些术语都是同义词。

假设您的应用程序具有三个流程外依赖项:数据库,文件系统和支付网关。 典型的集成测试将仅包括范围内的数据库和文件系统,并使用测试双倍来替换支付网关。 那是因为您拥有对数据库和文件系统的完全控制权,因此可以轻松地将它们带入测试所需的状态,而您对支付网关的控制程度却不同。 使用支付网关,您可能需要联系支付处理者组织以设置特殊的测试帐户。 您可能还需要不时检查该帐户,以手动清除过去测试执行过程中剩余的所有付款费用。

由于端到端测试在维护方面是最昂贵的,因此最好在所有单元和集成测试都通过后,在构建过程的后期运行它们。 您甚至可能只在构建服务器上运行它们,而不在单个开发人员的计算机上运行它们。

请记住,即使使用端到端测试,您也可能无法解决所有进程外依赖项。 可能没有某些依赖项的测试版本,或者可能无法将那些依赖项自动变为所需状态。 因此,您可能仍需要使用双重测试,以加强一个事实,即集成测试与端到端测试之间没有明显界限。

概要

  • 在本章中,我已经完善了单元测试的定义:
1.单元测试验证单个行为单元,2.快点3.并将其与其他测试隔离开来。

-隔离问题争议最大。 这场纠纷导致形成了两个单元测试流派:古典(底特律)流派和伦敦(模拟派)流派。 这种意见分歧会影响对单元构成的看法以及对被测系统(SUT)依赖项的处理方式。

1.伦敦学派指出,被测单元应相互隔离。 被测单元是代码单元,通常是类。 除不可更改的依赖关系外,其所有依赖关系均应在测试中替换为测试倍数。2.古典学派指出,单元测试需要彼此隔离,而不是单元隔离。 同样,被测单元是行为单元,而不是代码单元。 因此,仅共享的依赖项应替换为测试双精度。 共享依赖关系是为测试提供手段,以影响彼此的执行流程。
  • 伦敦的学校提供​​了以下优势:更好的粒度,易于测试互连类的大型图以及易于在测试失败后查找哪个功能包含错误的功能。

  • 伦敦学校的好处乍看起来很有吸引力。 但是,它们引入了几个问题。 首先,将重点放在测试中的类上是错了:测试应该验证行为单位,而不是代码单位。 此外,无法对一段代码进行单元测试是代码设计存在问题的有力迹象。 使用测试倍数不能解决此问题,而只能将其隐藏。 最后,虽然在测试失败后轻松确定哪些功能包含错误很有帮助,但这没什么大不了的,因为您通常仍然知道导致错误的原因-这是您最后编辑的内容。

  • 伦敦单元测试学校最大的问题是规格过度的问题-将测试与SUT的实施细节耦合起来。

  • 集成测试是至少不满足单元测试标准之一的测试。 端到端测试是集成测试的子集; 他们从最终用户的角度验证系统。 端到端测试直接与您的应用程序所使用的所有或几乎所有进程外依赖项保持联系。

  • 关于经典风格的经典书籍,我推荐肯特·贝克(Kent Beck)的“测试驱动开发:以示例”。 有关伦敦风格的更多信息,请参阅由测试指导Steve Freeman和Nat Pryce编写的“增长面向对象的软件”。 有关使用依赖项的更多信息,我建议由Steven van Deursen和Mark Seemann撰写的“依赖项注入:原理,实践,模式”。

单元测试chapter2相关推荐

  1. 嘿嘿,JAVA里第一次运行单元测试成功,立存

    按书上写的单元测试. 居然一次过,爽!!! package org.smart4j.chapter2.test;import java.util.HashMap; import java.util.L ...

  2. springboot项目使用junit4进行单元测试,maven项目使用junit4进行单元测试

    首先,maven项目中引入依赖 <dependency><groupId>junit</groupId><artifactId>junit</ar ...

  3. 写算子单元测试Writing Unit Tests

    写算子单元测试Writing Unit Tests! 一些单元测试示例,可在tests/python/relay/test_op_level3.py中找到,用于累积总和与乘积算子. 梯度算子 梯度算子 ...

  4. 写单元测试应该注意什么

    写单元测试应该注意什么 转载于:https://www.cnblogs.com/yishenweilv/p/10899695.html

  5. Atitti mybatis的单元测试attilax总结

    Atitti mybatis的单元测试attilax总结 版本mybatis 3.2.4 /palmWin/src/main/java/com/attilax/dao/mybatisTest.java ...

  6. java 中的单元测试_浅谈Java 中的单元测试

    单元测试编写 Junit 单元测试框架 对于Java语言而言,其单元测试框架,有Junit和TestNG这两种, 下面是一个典型的JUnit测试类的结构 package com.example.dem ...

  7. android 找不到类文件,Android Studio单元测试找不到类文件!

    就是一个方法里面逻辑比较多,查数据库,循环等等.比较复杂,我想测试一下他.是没有返回值的,我想看运行完成之后看看最后里面的变量是不是对的 如果跑整个程序的话就太慢了, 编译,运行, 登陆 等等.太长了 ...

  8. java单元测试启动类配置_Springboot 单元测试简单介绍和启动所有测试类的方法

    最近一段时间都是在补之前的技术债,一直忙着写业务代码没有注重代码的质量,leader也在强求,所有要把单元测试搞起来了 我把单元测试分为两种 一个是service的单元测试,一个是controller ...

  9. JUnit单元测试依赖包构建路径错误解决办法

    JUnit单元测试依赖包构建路径错误解决办法: 选中报错的项目文件夹→右击选择属性(ALT+Enter)→java构建路径→库→添加库→JUnit→选择合适的Junit库版本.

最新文章

  1. html5 settimeout,计时器setTimeout()
  2. 2.3.4 信号量机制
  3. linux-shell命令之chown(change owner)【更改拥有者】
  4. 3d文件与html结合,js和HTML5怎么结合?
  5. linux位置变量的应用,llinux中变量的运用
  6. win7充当无线路由器
  7. ansys linux运行_如何在linux系统下启动workbench。谢谢啦。 - 仿真模拟 - 小木虫 - 学术 科研 互动社区...
  8. Anaconda下载速度慢,用清华镜像
  9. PHP开发环境phpnow的详细安装步骤
  10. 2048C语言源码linux
  11. 阿里飞冰的介绍以及使用
  12. android手机变windows8,安卓手机如何把手机界面投屏到windows8/10电脑上
  13. Java读取Excel,03版本和07版本
  14. 怎么将png图片缩小?教你在线压缩png图片的方法
  15. 计算机未来规划范文200,计算机职业规划书范文
  16. python-基本的图像操作和处理
  17. php读取西门子plc_西门子PLC读取/修改V90 PN参数
  18. 接口和抽象类之间有什么区别?
  19. K-Pop 粉丝是新的匿名者
  20. mysql 大数据查询使用 exists,或者not exists

热门文章

  1. 线程状态转换图及其5种状态切换
  2. redis中使用GeoHash
  3. 朋友圈评论发html,微信评论怎么发图片(微信朋友圈评论可以发表情包啦)
  4. 【前端前沿看点】React和Vue深度对比
  5. Sketch插件介绍
  6. 关于报错connection holder is null
  7. 数据结构 笔记--向量 C++ 语言版 邓俊辉老师
  8. 数码显示实验报告C语言,数码管动态显示实验报告
  9. 洽谈 5G 时代“音视频”开发前景及学习方向
  10. Mac简单易用的复制软件——“TouchCopy”