第 1 章 自动化测试让你重获自由

第 1 章 自动化测试让你重获自由

当我们编写的应用成功上线后,每个人都会获益良多。产品故障的成本非常高,应该尽可能降低这种风险。如今,技术发达、信息发达、信息透明度高,一旦应用发生故障,全世界都会知道。有了自动化测试,我们就能够尽早发现故障,降低风险,从而开发出健壮的应用。

自动化测试对代码设计产生了深远影响。它促使我们编写模块化、高内聚、低耦合的代码,让代码易于修改,这有利于降低变更的成本。

也许你急于着手编写代码,但了解一下为什么要用到自动化测试以及可能面临的一些阻碍,可以让你为深入学习后续章节的技术做好准备。我们来快速探讨一下自动化测试的优势以及它带来的挑战,并研究一下如何运用短反馈循环。

1.1 变更的挑战

代码在其生命周期内会被多次修改。如果一位程序员告诉你他的代码从创建起就从未修改过,这就意味着他的项目后来被取消了。如果一个应用想要存续下去,就必须不断改进。我们会不断增强应用的现有功能,添加新功能,并且经常修复应用中的bug。每次变更都面临着一些挑战:

  • 变更的成本应该合理
  • 变更应该带来正面的影响

我们依次讨论一下这些挑战。

1.1.1 变更的成本

良好的设计应该是灵活的、易于扩展的、维护成本低的。但是如何能够分辨出这些特性呢?我们不能等着查看设计的结果来了解其质量——这样可能已经太晚了。

测试驱动的设计有助于解决这个问题。在这种设计方法中,我们首先创建一个初始的、主要的、策略性的设计。然后,通过一系列策略步骤,运用一些基本的设计原则(参见Agile Software Development: Principles, Patterns, and Practices [Mar02]),进一步改善设计。此外,测试还可以提供持续的反馈,以确保代码的设计符合需求。测试促进了良好的设计原则——高内聚、低耦合、模块化的代码,以及单一抽象层次——这些特征让我们能够负担得起变更的成本。

1.1.2 变更的影响

当我们修改代码时,常常听到的令人畏惧的一个问题就是“这有用吗”。而我们开发人员通常会回答“希望如此”。希望自己的努力能有个正确的结果并没有错,但是我们可以争取做得更好。

因为软件并不是一个线性的系统,所以某一处的修改可能会破坏别处。比如,如果错误地修改了数据格式,那么这个小错误就可能会影响到系统的多个部分。如果在修改之后,另一个完全不相干的部分却出现了故障,就别提多让人沮丧痛楚的了。这会让人很尴尬,因为客户会认为我们很不专业。

修改代码后,我们应该很快知道之前可以正常运行的代码是否仍然能够运行。因此,我们需要快速的、短期的、自动的反馈循环。

1.2 测试与验证

使用自动化反馈循环并不意味着就不需要手动测试了。

并不是非此即彼,我们需要正确地结合这两者。我们将定义两个需要加以区分的术语,即测试验证(testing vs. verification)。

测试需要敏锐的洞察力。应用的可用性如何?是否直观?用户体验如何?操作流程合理吗?有没有哪些操作步骤是可以省略的?测试时需要提出诸如此类的问题,并且要对应用的核心功能及其局限性有所了解。

另一方面,验证需要进行确认。代码是否实现了预期的功能?计算结果是否正确?当代码或配置被修改后,应用的运行是否符合预期?第三方库或者模块的升级是否导致应用崩溃?这些是验证应用的行为时需要关注的方面。

手动测试也是非常重要的。在最近的一个项目中,在数小时编程-自动化验证的循环之后,我手动运行了应用。页面一出现在浏览器上,我立马就想修改好几个地方了。这就是观察者效应。我们需要经常手动运行并测试应用。但请记住,这么做的目的是了解应用,而非验证应用。

还是在刚才所说的那个项目中,在产品发布前,我花了几个星期来修改数据库。一运行自动化测试,就出现验证失败。甚至还没有在Web服务器上运行,我就马上进行了修正和重新验证。自动化验证大大节省了我的时间。

测试与验证并行

 通过手动测试来洞察应用,通过自动验证来修改设计和确认代码始终符合预期。

1.3 采用自动化验证

采用自动化验证(或者宽泛地称为自动化测试)的方式在整个行业中的差别是很大的。总体来说,采用方式有以下3种。

  • 不采用自动化,纯手动验证。这主要是因为团队拒绝自动化。每次修改后,他们都在为验证应用而奋斗,饱尝验证结果出错的苦果。
  • 主要在UI层采用自动化测试。很多团队已经意识到自动化验证的必要性了,但他们主要将自动化验证用于UI层。这导致了半吊子的自动化,我们稍后将会对此进行详细探讨。
  • 在正确的层次上采用自动化测试。成熟的团队就是这么做的,他们不仅认识到自动化测试的必要性,而且又更进一步。通过在底层进行更多的测试,他们在应用的不同层次上投入了短反馈循环。

将自动化测试极端地集中在UI层会导致蛋筒冰淇淋反模式(ice-cream cone)1

1http://watirmelon.com/2012/01/31/

得到这个反模式的一个原因是,团队成员之间意见不一致。由于急着实现自动化,团队聘用了自动化测试工程师来负责创建自动化测试用例。但问题是,程序员往往不够支持测试工程师的工作,并且没有提供应用不同层次的测试钩子,这些事情他们以前都是不用做的。因此,自动化测试工程师只能为他们可以接触到的部分编写测试,通常是GUI和外部API。

主要集中在UI层的测试有很多缺点2

2http://martinfowler.com/bliki/TestPyramid.html

  • 非常脆弱。UI层测试经常失败。UI是应用中最容易变动的部分。当依赖的代码被修改时,它就会发生变化。客户和测试人员也经常会针对UI发表自己的看法,所以UI是经常需要修改的。每次修改UI后就进行测试是相当费力的,比底层测试要费力多了。
  • 过多的活动部分。UI层测试通常需要Selenium这样的工具,而且需要在不同的浏览器中运行。保持这些依赖工具启动并运行需要花费很多精力。
  • 非常缓慢。这些测试需要运行整个应用,其中包括客户端、服务器端、数据库,以及其他外部服务。在整个集成环境下运行上千个测试肯定要比单独测试耗时更多。
  • 难以编写。我的一个客户曾耗时6个多月为一个简单的交互编写UI层测试。我们最后发现,相较于产生这一结果的客户端JavaScript的运行,测试更花时间。
  • 无法隔离问题域。当UI层测试失败时,我们只能确定出现了问题,但是很难知道具体是哪里的问题。
  • 无法防止在UI层中包含业务逻辑。众所周知,在UI层中包含业务逻辑是相当不好的,然而业务逻辑往往渗透在UI层中。UI层测试对于解决这个问题毫无帮助。
  • 无法改进设计。UI层测试无法防止所谓的“大泥球”——模块化代码的对立面。

Mike Cohn在[Coh09](Succeeding with Agile: Sofware Development Using Scrum)中提出了测试金字塔的理念,即底层测试最多,高层次的端到端测试最少。

我们应该遵循这个测试金字塔,避免陷入蛋筒冰淇淋反模式的陷阱。为底层代码编写更多的测试好处多多。底层测试运行速度更快、更易编写,同时反馈循环更短。此外,底层测试有助于模块化设计,因此更容易隔离和定位问题。

我们将在本书中遵循这个原则,在合适的层次编写测试,即单元测试最多,功能测试其次,端到端UI层测试最少。

自动化验证是必须进行的

 未进行合理的自动化测试就部署一个很重要的应用会大大增加经济成本。

既然合理的自动化测试如此重要,那为什么很多开发者没有这么做呢?接下来我们就探讨一下这个问题。

1.4 为什么难以验证

说起来容易,做起来难。开发人员发现,以下两个问题总是会导致自动化测试很难进行。

  • 代码设计得很差。
  • 开发人员不知道代码是如何运作的。

这些话听起来可能有点刺耳,但我无意冒犯任何人。相反,我希望你能理解这两个关键的原因,并找到解决方法。

为遗留代码编写自动化测试困难重重。遗留代码通常具有非模块化、低内聚和高耦合等特性,而这些特性都是不良设计的标志。即使想法很好,但为这类代码编写测试是很难实现的。

要想具有可测性,代码必须具备高内聚、低耦合的特征。如果一段代码执行多个任务(这意味着低内聚),那么就需要对这段代码进行更多测试,这基本上是不可能完成的。如果代码直接与服务相关联(这意味着高耦合),那么就很难编写确定性测试。自动化测试是否可行与代码设计密切相关。

此外,如果我们不知道一段代码的运作方式,就很难为其编写测试。我们通常根据教程来使用库或者框架,能够快速创建代码并运行,但可能没有花时间了解其各个部分是如何集成的。编写更多的代码来生成结果是很容易的,但被要求为这些代码编写自动化测试时,程序员通常会感到很困惑。对代码缺乏足够的认识,尤其是低于它们所依赖的抽象层的那些代码层,是导致自动化测试难以编写的主要原因。

有时我们可能认为编写自动化测试是无法实现的,但请不要将缺乏知识和技能与不可能性相混淆。我们无法做到不可能实现的事,但有办法解决知识或技能的不足。也许现在不知道要怎么做,但我们可以向其他人学习,还可以与其他人合作以实现目标。自动化测试是一门技术,只要我们有心,它是很容易学习和实现的。

1.5 如何实现自动化测试

对程序员来说,编写代码不是什么难事,但在编写代码前先编写测试却是相当困难的。

原因之一是,编程是每天按照顺序进行的一系列小实验,这是个连续的过程。编写代码,发现问题,重读代码,然后再换一种方式不断进行尝试、纠错,直到代码正确运行。这正是我们编写代码的方式。

当对代码还没有概念时,又怎么能够先编写测试呢?这看起来似乎违背直觉,但其实是有解决方法的。

首先,细化测试。如果测试看上去很难实现,很可能是因为我们一次做得太多了。为了使用测试优先的方式来实现一个功能,我们必须进行一系列的测试,可能是3个、5个,甚至15个。将每个测试视为楼梯的一个台阶,虽然每个台阶都很小,但间隔合理,因而能让我们一步一步往前进。

其次,分而治之。如果很难为一个功能编写测试,那么这个功能的设计很可能还不够内聚或者太过耦合。我们可能需要将设想的这个功能拆分为一些更小的功能,再分别为这些功能编写测试,以此驱动这些功能的开发。

最后,采用spike解决方案3。如果你正在设计的功能和以前实现的很相似,那么就一次编写一个测试,然后编写最少的代码来通过每个测试。相反,如果你对一个功能的实现非常陌生,即这个功能的实现是非常复杂的、异步的或者非常难懂的,那么不要立即编写测试,而要建立一个独立的可执行原型。我们来进一步讨论一下具体的做法。

3spike解决方案是极限编程中的一种解决方案,即如果在设计中碰到困难,那么就立即为这部分建立一次性的可执行原型。——译者注

将当前项目放在一边,转而开发一个小型的spike项目,这样你就可以在这个项目中随意实践。此时不用关心代码质量或者测试问题。只需要让它运行起来,得到你想要的结果。一旦这部分代码正确运行了,就可以鼓起勇气将其丢弃。接下来回到原来的项目中,开始编写测试,以此驱动代码设计。你很快就会发现先编写测试并没有那么可怕,也会发现之后的代码设计和你在spike中所做的大不相同。

1.6 小结

手动测试和自动化验证都有其价值。测试有助于我们更深入地了解自己编写的代码。没有自动化验证,就不可能维持合理的开发速度。所有重要的应用都需要在正确的层进行自动化测试。虽然自动化测试需要花费时间和精力,但它仍是我们需要投资的一个技能。在编写代码之前先编写测试,如果碰到困难,可以尝试spike解决方案。

正如本章中所说的,自动化测试非常重要。在第一部分中,我们将探讨为客户端和服务器端代码编写自动化测试的基本技巧。

目录

  • 版权声明
  • 献词
  • 本书赞誉
  • 致谢
  • 前言
  • 第 1 章 自动化测试让你重获自由
  • 第一部分 创建自动化测试
  • 第 2 章 测试驱动设计
  • 第 3 章 异步测试
  • 第 4 章 巧妙处理依赖
  • 第二部分 真实的自动化测试
  • 第 5 章 Node.js测试驱动开发
  • 第 6 章 Express测试驱动开发
  • 第 7 章 与DOM和jQuery协作
  • 第 8 章 使用AngularJS
  • 第 9 章 Angular 2测试驱动开发
  • 第 10 章 集成测试和端到端测试
  • 第 11 章 测试驱动你自己的应用
  • 附录 网络资源
  • 参考文献