这里的测试指的是自动化测试,从软件的本质上看,测试的自动化乃是测试方面的软件开发,万变不离其宗,这也意味着那些凡是属于软件开发的定律或者原则也同样适用于测试自动化。对于没有写过代码或者代码经验较少的人来说,或许这其中的道理不能一眼瞧得出来。

  通常情况下软件开发的很大一部分开销是维护??修修补补,更新不断。软件的可维护性强,则开发成本低,同理,测试的自动化开发成功与否也很大程度上体现在它的可维护性成本大小上。我接触过的很多试图尝试引进自动化测试的机构没几个月决定放弃自动化测试,问之弃因,你会发现大多是因为自动化脚本过于不稳定以及随之衍生的难维护性。举个例子,界面上重命名一个按钮会导致大批的测试用例失败,而与此同时花费在调通和更新这些用例身上的时间成本又太高。

  有些团队或机构在自动化测试上取得了成功,难道他们的自动化可以避免掉这样的维护费用问题?当然不可能。而成功与失败的团队之间一个重要的区别是:在对待测试开发的维护问题上,失败者往往是被昂贵的维护费用吓住而放弃自动化计划,而成功者则是从一开始做足了应对措施。那些在自动化取得成功的团队懂得测试即开发这一道理,明白测试开发一旦开始,维护在所难免,所以他们会深思熟虑,想方设法降低维护成本。

  软件需求的变更和系统实现的变更会影响测试,需要测试做出相应的调整,这二者任意一种变更都可能导致一系列的自动化测试失败。如果一些自动化测试不能同步新的软件变更和产品新特性,那么它们将会被淘汰,其测试结果也不会得到运用。而要使其回归正常,我们必须不断调整测试以配合需求和系统实现的变更。维护的成本开始显山露水。

  因此如果需求和实现的变化是必然的,那么降低自动化测试维护成本的方法只有一个,即编写适应性强的测试脚本。

  暴露太多无关紧要的细节或者重复这两大关键因素使得修改代码的困难大大增加,无数惨痛的经验教训让软件开发人员对此深有体会,对于那些正在从事和将要从事的自动化测试开发者们,也肯定不想重蹈覆辙。

  验收测试和系统的任务

  验收测试用来检测一个系统是否正确履行了某一特定任务。也意味着,验收测试的核心是关注它所要验证的功能点是否正确,而不考虑用了何种技术、何种方法去测试。

  现在假定我们要测某个系统的创建账号这个特性,系统通过传递给 Create 命令用户名和密码来创建新账号。创建账号特性的功能之一是验证密码的有效性。一个合法的密码长度必须介于6~16字符之间且至少包含一个字母、一个数字以及一个标点符号。如果用户提交的密码合法,Create 命令创建成功并报告 Account Created;反之,Create 命令不会执行创建过程,同时报告 Invalid Password。这是功能职责的本质。无论软件系统以何种技术实现,Web 应用也好,GUI 桌面应用也罢或者是命令行执行的程序,也不管会不会有人像德州电锯杀人狂里的休维特一样,挥舞疯狂的电锯恐吓要锯断那些输错密码童鞋的指头,总之,系统需执行此项职责(密码检查是系统必须实现的职责)。

  无关紧要的细节


表 1 不良自动化测试用例脚本

  列表 1 展示的是一段不良自动化测试用例脚本,该测试用例用来检测 Create 命令的密码有效性检查这一职责。
  这段测试脚本问题很多,一眼望去,明显的是可读性很差,我们看到第二行 The create command validates passwords,这是测试的标题,表明该测试的测试点和职责,但往下读时,我们会发现,里面充斥了太多累赘的单词和烦人的诸如“{$@^”这样的符号,让人不知其所言。

  仔细看一下,我们可以挑出来几个密码,比如 1234!@$^!紧接着再加把劲,啊哈,我们会发现一些密码会导致 status 值为 Invalid Password,而另一些会使得 status 值为 Account Created。从另一方面看,我们可能也会注意不到上述内容因为该测试脚本在密码和状态 status 之间夹杂了太多的实现细节,或曰:测试噪声。试想,这些特殊的符号$、@、^以及单词 Run、Ruby、fred 究竟和密码及其有效性有什么关系!对于自动化测试的脚本来说,从用例的可读性和可维护性角度看这些都是无关紧要的过程实现细节。

  过多无关紧要的细节是怎样毁掉可维护性的?假设我们的系统安全分析师指出六位长度的密码本身不安全。于是为了增强安全性,我们将密码长度下限由六改成十,这是一个典型的需求变更,请思考,这时候列表 1 的测试脚本哪里需要修改?怎样改?答案恐怕没那么简单。

  让我们再来考虑一个更有挑战性的需求变更。假如我们想让系统管理员能够为任一种情况设定具体的密码长度长与短值。这时候该怎么修改刚才的测试脚本?答案还是无法一眼看出。恐怕没那么简单。

  这其中的原因“恐怕”在于测试脚本没有清晰表达它所要测试的功能职责。当看不出一段测试用例脚本的本质 ,通常意味着需求变更之时测试人员会需要花数倍的代价来修改原测试脚本。

  因此,为方便识别本质要隐藏非必要细节的,以使自动化脚本用户更容易看到测试的本质,在上面创建用户的例子中,大多数非必要的细节是如何调用 Create 命令。该系统是基于 Ruby 的命令行程序,现在让我们再次返回列表 1 的测式脚本里解读一番,黑色字体加深的第一行告诉自动化测试框架 Robot 启动 Ruby 解释器,加载被测程序文件 app/cli.rb,并调用 Create 命令,参数值为用户名 fred 以及密码 1234!@$^,后命令返回的结果存在变量${status}中,呵,数数不必要的实现,细节至少有5~6个之多!

  再来看字体加深的第二行,实现命令返回值和期望值 Invalid Password 的比较,虽然看起来比刚才那一行较容易理解 ,但措辞笨重,并且过分的语法细节容易分散人们的注意力。

  通过 Robot 自动化框架我们可以把实现细节提炼成关键字(Keyword),使之以类似子函数的形式为测试用例脚本调用,一个完整的自动化测试用例便可由数个关键字组合而成。

  现在我们演示如何使用关键字来隐藏不必要的实现细节。一个可行的方法是问自己这样一个问题:假设自己对被测系统实现一无所知,该如何写出自动化测试脚本的第一步?是的,即使对实现一无所知也无大碍,我们只需知道我们要测试创建用户这个产品特性??被测系统显然要提供的功能。继而我们知道创建用户即是被测系统的必要职责,而且从系统需求分析可知创建用户需要提供用户名和密码。

  基于以上所述,可能这样修改测试:

  Create Account fred 1234@!$^

  当然修改后的脚本可能还有其他一些问题,我会在稍后部分接着讨论。

  再看看加深的第二行,验证创建用户命令返回的结果是否为 Invalid Password,顺着上面修改的思路则可以变成:Status Should Be Invalid Password。

  两行合并,看一看整体效果:

  Create Account fred 1234@!$^

  Status Should Be Invalid Password

  原来的一步经过一次提炼现在看起来简洁多了,可读性也变强了,我们很容易发现这两行的逻辑上的联系:系统必须告知输入的这一密码是无效的。

  现在还无法运行新的脚本,因为测试框架 Robot 找不到关键字 Create Account 和 Status Should Be 的定义,两个关键字的 Ruby 实现代码如表2。


表 2 两个关键字的 Ruby 实现代码

  字体加深的代码行创建了一个名为 Create Account 的关键字,需要两个参数 user_name 和 password。关键字函数的主体只有两行代码,第一行加载被测程序并调用 Create 命令,第二行保存返回值,对比之前的原测试脚本,我们发现主体第一行代码和表 1 的加深第一行实现了同样的功能,非必要细节即隐藏于此。

  你或许已经发现了关键字的实现代码里引入了更多的语法和特殊字符。这不用多虑,通过把细节抽象成关键字, 我们的测试脚本看起来整洁多了,可读性大大增强,现在我把更新过的测试脚本贴上如表 3 所示,更直观展示了修改后的效果。


表 3 更新过的测试脚本

  虽然某些程度上增加了关键字这一部分的代码,但获得了整个测试用例脚本的干净清爽,这一做法是值得尝试的。

  重复

  上面我们已经学会通过提炼可复用的关键字改进了测试脚本,但还存在其他问题。一个问题是我们之前提到的,即每隔一个测试步骤包含用户名 fred。另一个更大的问题是重复,从表 3 修改过的测试来看,每一对关键字(Create Account 和 Status Should Be)组成一步验证测试,每一步都要提交一个不同的密码并与比较系统返回的状态值和期望值,我们看到,除了输入的密码和期望状态值不同之外,其他部分基本都是一样的。