重复的代码将毁掉可维护性。现在假设我们的用户交互分析师指出产品系统的其他部分并不要求用户创建账号(Create)而是要求他们注册(Register),这在同一个系统里出现了用户交互接口和使用术语的不一致,而用户交互分析师坚持整个系统应该杜绝这种不一致性,于是我们决定把 Create 命令变成 Register。

  这样一来,对测试会产生的影响有多大?我们封装了关键字 Create Account 来创建用户,现在看样子只需要把关键字的实现部分中调用 Create 命令的地方改成 Register 可以了,但这样一来的又一个问题在于我们的测试的关键字和被测的功能也出现了不一致的叫法,这会使人困惑。或许以后我们每次验收测试跑完之后,都需要向这些测试报告的经理或者市场人员解释这些关键词。

  为了保持术语一致性,好的做法是修改我们的测试用例。上面的测试用例中,至少有八个用到 Create Account 关键字的地方可能都要改成 Register。还有两个关键字也都用到了 Create Account。而现实会更残酷,可能有成千上万的测试步骤都调用了那个关键字。由此,我们得出结论:重复会增加维护成本。

  重复往往预示了潜藏在测试中的某一重要概念。当这样的重复不是发生在个别测试步骤而是一系列的步骤的时候,情况更是这样。

  思考一下表 3 中的测试脚本,看看前面两行究竟说明什么。没错,它们核实创建用户命令是否拒绝 1234!@$^这一密码。那再来看看第9~10两行。这两行来证实创建用户命令接受!C2456这一密码,再进行一次抽象概括,我们惊奇地发现,原来这两行测试的本质便是接受(Accept)和拒绝(Reject)。但遗憾的是在表 3 的测试中,这一本质却被埋没了。接下来我们利用两个新的关键字来使概念明朗化,如表 4 所示:


表 4 用两个新的关键字来使概念明朗化

  这两个新的关键字不仅将重写我们的测试脚本,同时也给接受密码和拒绝密码下了定义:接受密码即调用被测系统的 Create 命令,系统报告用户创建成功;拒绝密码即调用 Create 命令,系统报告密码无效。

  如此一来再次经过修改的测试脚本如表 5 所示,减少了重复并且更能体现被测功能的职责,即密码的接受或拒绝。


表 5 减少了重复的重写测试

  让我们捋一捋刚才的思路,总结一下该部分。首先我们经过分析测试代码的重复部分,发现被测功能的两个基本概念??接受正确的密码和拒绝错误的密码。通过定义两个关键字,我们抽象并命名了这两个基本概念。后我们在测试用例中使用新的关键字,从而提升了测试的可读性以及可维护性。

  给本质一个有意义的名字

  经过一番“折腾”,现在表 5 给出测试已经能够比较清晰地表达测试的基本概念。而与此同时,后一点不够清晰的地方已经开始明朗起来。看看测试中的几个密码,我们不能马上弄清到底给出的密码无效在什么地方?那正确的密码是符合了什么样的规则吗?或许花些时间你可以找到问题的答案。这里涉及一个重点:任何花在思考测试的意义即其本质的时间都算作维护成本 。这个成本看起来微不足道,但是如果系统需求更改导致大面积关联的测试用例都要修改,这时累加起来的成本是巨大的。我的一些客户已经发现这个问题的严重性,道理很明白,千里之堤,溃于蚁穴。

  在上述的测试例子中,我所选取的每一个密码都有特定的目的,也说每一密码的本质都是和被测系统功能的某一个需求有关。比如 1234!@$^这个密码,它不含字母, 因而这个密码的本质可以这么描述:一个不包含字母的密码。

  我习惯给每一个本质赋予具有意义的名称,在测试代码中给本质命名的是变量。有时候我也创建变量,给它命名一个富有表现力的名字,再给它规定一种能体现其名的价值。如下,我定义了一个变量用来储存没有字母的

  密码。


表 6 在测试脚本中使用变量,节省空间

  然后在测试脚本中使用变量,节省空间,这里略去了其它变量的定义过程,如表 6 所示,每一个密码都以变量的形式表达其代表的本质意义。至此,离我们开始设定的目标已经很近了,但依然有可以再优化的地方,我将进一步把原来一个测试按照密码不同属性拆分成多个测试用例,如表7。


表 7 把原来一个测试按照密码不同属性拆分成多个测试用例

  现在,只需看一眼便可知每一个测试用例或者每一步的意义何在。这里,重要的需求概念被清晰精炼地表述了出来。

  现在假定新需求改变了密码极限长度,由于每一个需求和被测功能都由测试用例清晰地显示出来,我能够很快定位哪一个测试用例需要修改。并且由于每一个测试数据也即密码都以有意义的变量的形式储存,我们能够很快的找到需要修改的变量进行重新赋值。先前对测试代码所做的重构带来的好处显而易见,这里再一次强调测试即开发的主旨。

  让测试经得起测试:应对系统主要实现架构的改变

  在前面部分我们努力让测试能够更灵活地自适应需求变更,可如果是系统的主要实现架构发生了变化呢?测试会受到什么影响?为了找出答案,我们通过改变一点实现的细节??通过 Web 页面创建用户的方式取代之前的命令行调用 Create 命令的方式。现在创建用户只需要打开创建用户的 Web 页面,输入用户名和密码,然后点击创建用户按钮,由页面打印创建结果。严峻的问题来了,我们的测试该如何修改?

  还记得我们之前封装不必要细节并提炼了两个关键字 Create Account 和 Status Should Be。这两个关键字封装的细节是如何调用命令行执行 Create 命令以及获得结果报告。很明显,我们肯定要重写这两个地方,因为现在需要通过 Web 页面操作才能实现。


表 8 修改后的关键字实现

  表 8 是修改后的关键字实现。我们修改了与系统交互的实现,即通过使用开源 Web 测试工具 Selenium 进行页面访问和操作,那么现在的问题是对于那几个具体的自动化测试用例,我们还需要修改什么?答案是什么都不需要,此次修改工作已完毕。通过改变几行代码,使我们的自动化测试轻松运行在变化了的实现架构的系统上,这往往是成功和失败的自动化测试之间的区别。

  与此同时,回到现实世界

  在真实的测试中,你可能要做更多的工作以应对系统实现架构的变化,你可能不止需要修改两个关键字。但只要你创建了级别较低的关键字,将其他代码和与系统交互的细节分开, 那么你所需要做的只是修改这些关键字而该测试用例照常运行不需要改动【译者注:如果你看到 Martin Flower 的《重构》一书,应该明白这样一条重构原则,保持接口不变,改变底层实现】。

  真实的项目里很多实现架构上的变动将会对测试开发工具提出更严酷更颠覆的问题。 但即使是坏情况,你依然可以使用先进的开源测试工具,用以解决很多重复的问题,帮助你撰写能够清晰表达测试本质的用例和脚本。

  再次强调,一定要记住其中的本质内容:只有消灭重复的无关紧要细节,让测试清晰表达被测系统功能职责,才能在发生系统需求和实现变更的情况下轻松应对以便降低自动化测试维护的成本。这也正是我们衡量自动化测试开发成功的标志。