读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率

时间:2022-03-20 06:53:06

转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting3/

 

所谓自动化测试,就是“自动化”+“测试”。自动化本身显然不是自动化测试的全部,在我们解决了测试脚本自动执行的问题之后,还是要回到测试本身,解决如何进行分析验证的问题。测试结果的分析对于测试来说非常重要,往往需要收集大量的数据,包括屏幕截图和产品日志,并根据具体需要分析各种不同的信息,例如验证点,测试实际步骤和自动测试调试信息等等。可以通过精心准备测试用例、合理输出验证信息、筛选显示测试结果、自动记录测试实际步骤等手段来全面提高分析测试结果的效率。本篇文章从准备测试用例开始,介绍了验证信息的输出,Notes 自动测试日志的各种功能,并对每项功能的实现原理做深入介绍。

准备准确的测试用例

测试用例是测试脚本的基础,不合理的测试脚本既不利于自动化,也不利于测试。而好的测试脚本则要做到描述简洁准确,操作步骤清晰,测试目的明确易懂。由于在编写测试用例时,产品一般没有完成,测试人员可能并不清楚执行用例的每个步骤,所以用例容易写得很模糊。手工测试人员执行时也许没有太多困扰,因为他了解设计文档,知道应该执行什么样的步骤,但是自动测试人员并没有这么清楚,依照模糊的用例编写测试脚本往往无从下手。所以,在开始编写测试用例之前,往往需要跟测试人员沟通,完善并得到准确的测试用例,这样才能做到有的放矢。

那么什么样的测试用例是描述准确,易于实现的?让我们通过一个测试 Notes 中的打开应用对话框的例子来看一看。

图 1. 测试打开文件对话框

读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率

例 1A. 模糊的测试用例

  1. 打开“Open Application”对话框
  2. 验证使用对话框可以打开 Server 端的用户邮箱
例 1B. 准确的测试用例
编号 步骤 验证
1 点击菜单:File-> Open Application
2 在 Server Combobox 中输入用户邮件服务器地址
3 点击“打开”按钮
4 在 File Name Editbox 中输入邮箱路径名称
5 点击“打开”按钮 验证用户邮箱成功打开

显然,对于一个有经验的测试人员,可以很轻松的理解例 1A 中的描述,但是对于自动测试人员来说,这种描述不够细致,并且容易带来歧义的。我们需要知道通过何种途经打开文件对话框?怎样切换到 Server?怎样在 server 上定位到用户邮箱应用?只有当我们把这些都明确下来之后,测试脚本才是准确的,并且易于实现的。

记录关键测试步骤和结果

准备好了用例,我们就可以开始编写测试脚本。基于准确的测试用例描述和灵活的 IBM 框架支持,我们可以轻松的把测试步骤翻译成测试脚本。同样以脚本例 1B 为例,我们可以可以得到例 2 中的脚本代码:

例 2. 初步的测试脚本
编号 步骤 验证
1 点击菜单:File-> Open Application Menu.mOpenApplication.pick()
2 在 Server Combobox 中输入用户邮件服务器地址 OpenAppDlg dlgOpenApp = new OpenAppDlg();
dlgOpenApp.getServerCombo().setText(User.mailServer)
3 点击“打开”按钮 dlgOpenApp.getOpenButton().click()
4 在 File Name Editbox 中输入邮箱路径名称 dlgOpenApp.getFileEdit().setText(User.mailFileName)
5 点击“打开”按钮 dlgOpenApp.getOpenButton().click() Logger.logCompare(User.name, MailTask.getOpenedMailOwner(),“VP: User’s mail file is opened”)

这是一个新手容易写出的测试脚本,这个脚本遵循 IBM 框架设计,完成了用例要求的步骤,并加入了对测试结果的验证。看起来符合测试用例的要求,不过,这个脚本还不够好。

我们说“自动化测试”=“自动化”+“测试”,作为程序执行正确性的验证工具,我们需要假设执行流程中的每一步都是可能出现错误的。比如说步骤一的点击菜单动作,就可能因为程序内的菜单映射错误而打开错误的对话框;再比如在步骤三中,可能因为网络超时或程序错误判断自身为离线状态而无法成功连接到服务器。还是以为回到测试结果的分析这一问题上来,一旦产品有了缺陷或者测试执行出错,我们如何快速定位问题么?很显然,测试人员不可能一直盯着测试脚本的执行,在自动化测试中,我们需要通过日志记录下关键步骤的执行过程和结果。

在通过日志分析结果时,通常我们需要知道两点:

  1. 步骤是否已执行;
  2. 被测程序是否正确做出反应。

对于第一点,通过在 App Object 层和 Task 层的增强,Notes 自动测试的框架能够自动输出信息,显示出执行了的步骤,这个功能本文后面会做深入介绍。而被测程序是否正确做出反应,则需要我们在测试脚本中进行检测并记录。实际上,当我们手工验证测试用例时,我们都会不经意的验证每一步的程序响应,并决定下一步的操作。作为“隐藏的验证点”,我们需要把他们补充到测试用例中。例 3 中给出了经过完善后的测试用例:

例 3. 完善后的测试用例
编号 步骤 验证
1 点击菜单:File-> Open Application 显示模态对话框出现,标题为“Open Application”
2 在 Server Combobox 中输入用户邮件服务器地址 验证输入结果正确
3 点击“打开”按钮 验证应用列表更新显示 Server 端应用内容
4 在 File Name Editbox 中输入邮箱路径名称 验证输入结果正确
5 点击“打开”按钮 验证“Open Application”对话框关闭
验证用户邮箱成功打开

相应的,我们在测试脚本的编写过程中也需要加入对各个步骤中程序响应的验证,并将结果记录到日志中。例 4 中给出了我们最终的示例测试脚本:

例 4. 完善后的测试脚本
Logger.logDetail(“1. 点击 File-> Open Application 菜单”);
Menu.mOpenApplication.pick();
OpenAppDlg dlgOpenApp = new OpenAppDlg();
Logger.logCompareFatal(true, dlgOpenApp.exist(LocalSettings.shortWaitTime),
  "验证:‘打开应用’对话框出现");Logger.logDetail("2. 在 Server Combox 中输入服务器地址");
dlgOpenApp.getServerCombo().setText(User.mailServer);
Logger.logCompareFatal(User.mailServer, dlgOpenApp.getServerCombo().getText(),
  "验证:服务器地址输入正确");Logger.logDetail("3. 切换到 Server");
dlgOpenApp.getOpenButton().click();
Logger.logCompareFatal(true, isContentOnServer(dlgOpenApp, User.mailServer),
  "验证:列表框中显示的内容为服务器上的内容");
Logger.logDetail("4. 在 File Name Editbox 中输入邮箱路径名称");
dlgOpenApp.getFileEdit().setText(User.mailPath);
Logger.logCompareFatal(User.mailPath, dlgOpenApp.getFileEdit().getText(),
  "验证:邮件路径输入正确");
Logger.logDetail("5. 点击确定,打开应用");
dlgOpenApp.getOpenButton().click();
Logger.logCompareFatal(true, dlgOpenApp.waitForNonExistence(LocalSettings.shortWaitTime),
  "验证:‘打开应用’对话框关闭");
Logger.logCompare(User.name, MailTask.getOpenedMailOwner(),
  "验证:邮件应用正确打开");

Logger 类是我们在 Notes 项目中定义使用的日志工具,可以支持多层级、多类型的日志记录。在例四中,我们用到了其中的三个日志函数:

  1. logDetail(expression):记录步骤基本信息
  2. logCompareFatal(expected, actual, vp):记录步骤验证结果,如果比较失败,停止当前用例。
  3. logCompare(expected, actual, vp):记录验证点,返回 boolean 值表示比较结果

我们之所以对关键步骤验证用到 logCompareFatal,是因为这些步骤不能正确执行的话,后面的验证和后续步骤也就失去继续执行的意义。与其浪费时间执行无意义的操作,不如停止用例,及时做出错误清理,同时记录错误信息,如截屏,和输出验证信息。通过完善的步骤检查和日志记录,我们可以在脚本发现错误的时候借助日志分析直接定位到出现问题的步骤。同时,您可能注意到我们在 exists 和 waitForNonExistence 函数中使用了超时设置,可以在保证被测程序有足够响应时间的情况下,尽可能的节约测试执行时间。如果您希望了解关于执行环境清理以及步骤间等待的更多细节,请参考本系列的第二篇文章。

日志的艺术

日志对自动化测试而言至关重要,而好的日志工具可以帮助我们提高测试脚本的输出质量以及问题定位分析效率。在 Notes 项目中,我们实现了自定义的 Logger 类以支持各种特定的输出需求。下面,让我们一起来看一看日志中都可以包括一些什么样的内容。

图 2. Notes 自动测试用例日志

读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率

如图 2 所示,Notes 的日志输出是一个 html 格式的文件,提供了验证点结果,用例结果和发生错误的截图。Notes 的日志工具支持以下功能:

  1. 自动记录测试步骤 (TESTPLAN)
  2. 隐藏和显示不同级别日志的选择按钮
  3. 按照关键词筛选显示日志内容
  4. 截图链接 (IMAGE)
  5. 被测产品日志链接 (IMAGE 和 NOTES LOG FILE)
  6. 记录生成日志的位置 ("CALLSTAMP")
  7. 记录日志生成的时间 (TIMESTAMP)
  8. 记录测试环境配置信息 (SETTINGS)

自动记录测试步骤:这一功能自动记录了每一步操作的执行过程,可以帮助我们分析步骤是否已经正确执行。在下一节中,我们会详细介绍。

隐藏和显示不同级别日志的选择按钮:以往分析日志的第一个感受就是日志内容太多太杂,在分析的过程中很容易丢掉主要的线索。另外,不同的读者需要的内容不一样,比如手动测试的人只关心验证点是否通过,而自动测试人员需要知道所有测试细节。Logger 通过提供 logError, logWarning, logDebug 等函数,把日志级别信息也同时输出到日志文件中。在检索日志文件时通过点击日志文件上方的选择按钮,我们就可以隐藏或显示相应级别的日志内容。

按照关键词筛选显示日志内容:跟上一点类似,提供更灵活的方式,按关键字筛选显示日志,便于查看测试人员关心的内容。关键字不仅可以是日志内容的某一部分,还可以是类的名字。

截图的链接:一图胜千言,截图是分析日志最直观的工具。当测试出现错误和异常时,Logger 能自动截图,把图片按日志文件的目录结构保存在电脑上,并且把文件路径作为链接显示在日志中。通过截图,测试人员可以更直观的了解出现的错误和异常。实现截图的代码如下:

例 5. 实现截图功能
BufferedImage capture = null;
Rectangle area = new Rectangle(x, y, width, height);
Robot robot = new Robot();
capture = robot.createScreenCapture(area);
FileOutputStream out = new FileOutputStream(filename);
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
encoder.encode(capture);
out.flush();
out.close();

被测产品日志链接:产品的执行日志。自动测试日志用来让测试人员发现产品问题,而进一步,让研发人员解决问题,通常还需要产品的日志。Logger 在被测程序出现不响应和崩溃时,会主动收集被测程序的日志文件,生成链接提供给测试和研发人员。在 Notes 项目中实际应用时,由于 Notes 的每行日志都会记录当前时间,Logger 会根据测试开始和结束时间,到 Notes 的日志文件中找到测试过程中生成的日志片段,并保存到自动测试日志目录的单个文件中。这样便可以将不同测试用例造成的问题隔离出来。

记录生成日志的位置:提供日志记录点的调用函数和具体位置。在自动测试人员分析日志时,往往会不清楚日志是什么地方产生的,这样在找出问题时需要花时间搜索代码。Logger 通过调用“newThrowable().getStackTrace()”,能够找到输出日志时,程序执行到的类名,方法名和行号。这些信息被分析后输出到日志文件,成为 CALLSTAMP 列显示在日志的右边。值得说明的是,在分析调用栈时,需要得到除了 Logger 之外的第一行,因为要记录的是调用 Logger 的函数,而不是 Logger 自己的函数。以下面的调用栈为例,这里的 EnableInstanceCheck 函数才是需要被记录到日志中的,而不是 logMessage。

例 6. 待分析的调用栈
Logger.logMessage()
Logger.logDebug()
SpellCheckTask.EnableInstanceCheck()
script2.testMain(Object[])

记录日志生成的时间:顾名思义。时间对于日志分析也是非常有用的信息,通过比较每行日志记录时间有助于找出性能相关的问题,例如当两次点击被测程序的时间比较长的时候,有可能是因为在第一次点击时的性能不够好,没有及时响应。具体实现比较简单,此处不再介绍。

记录测试环境配置信息:包括测试平台,产品版本和配置信息等。这些内容在 Logger 初始化的时候由 Logger 分析输出,可以帮助我们分析定位由于平台和配置差异导致的问题。

除了通过单一日志文件显示具体每一测试用例的信息,Logger 还能为每组测试用例生成测试报告,格式如图 3 所示。它提供了每个用例的结果,通过率等信息,还包含日志文件的链接

图 3. 测试报告

读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率

自动记录测试步骤

Notes 的 Logger 能够帮助我们自动记录测试步骤,下面让我们具体看一看这一功能的作用和实现方法。首先,记录测试步骤的好处有哪些呢?对 Notes 测试来说,有如下两点:

  1. 测试人员不需要读测试代码就能知道用例的测试步骤
  2. 测试代码往往有不同的逻辑分支。有了实际测试步骤,能够知道实际运行到的代码分支
图 4. 测试步骤记录示例

读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率

图 4 是测试中实际执行到的例子。其中典型的部分是:

Dialog: Replace - click on (Close) NPushButton

它表示点击了 Replace 对话框上面的 Close 按钮。短短一行日志包括下面 4 个方面的信息:

  1. 控件所在容器的名字
  2. 控件的名字
  3. 实际执行的方法名
  4. 方法接受的参数

如何自动找到这些信息?让我们先看一看这些对象是如何使用的:

例 7. 用例如何使用测试对象
// 测试对象的定义
class DlgReplace extends NNotesDialog{
    public NPushButton getClose(){
  return new NPushButton(id, this)
  }
}

// 用例中点击按钮
NPushButton btn = new DlgReplace().getClose();
btn.click();

从例 7 中控件与对话框的定义不难找到我们需要的四项信息:对话框叫 Replace, 按钮是 Close, 方法是 click,并不需要参数。问题再于如何将这些信息收集起来,并且在需要的时间输出到日志文件里。我们的解决方案分别如下:

  1. 构造控件时,记录调用栈 ( 例 8),从中找到容器和 getter 方法的名字:Replace 和 Close。之所以我们不在控件调用 click 方法时,使用控件内部的方法获得控件容器的类名,有两个原因:
    1. 容器有可能会互相嵌套。测试人员最关心的是最*的容器名字,比如对话框
    2. 不是所有控件构造时都传入了容器的引用。例如 web 控件通常用 xpath 定义,构造时并不需要指定容器,这样的控件没有内部方法获得控件容器的信息。
例 8. 构造控件的调用栈
NControl.<init>(ControlID,String,INControl)
NPushButton.<init>(ControlID,INControl)
DlgReplace.getClose()
  1. 调用控件方法时,记录调用栈,从中取得第一次调用时的方法名。
例 9A. 点击按钮时的调用栈
NPushButton.click(Point)
NPushButton.click ()
script2.testMain(Object[])
例 9B. 操作控件时的调用栈
NComboBox.click()
NComboBox.clearText()
NComboBox.setText(String)
script2.testMain(Object[])

当执行自动测试到 DlgReplace.getClose().click() 时,函数调用栈如例 9A 所示。显而易见,最后调用的 click 方法,对应了该次操作。然而大多数情况并没有这么简单,比如在文本框中输入文字时需要执行若干子步骤,包括点击控件,清除文字,和输入文字。这些步骤都在 setText 方法中调用。如例 9B 所示的调用栈。控件的方法可能会相互嵌套,只有第一次调用,才是最便于理解执行步骤的。我们在 click 函数里分析调用栈,能知道调用 click 方法是为了往控件上输入文字,然后记录成 setText。值得注意的是,如果把每个调用栈都转成步骤输入到日志的话,会包括重复并且很有歧义的信息。这就需要引入一个机制避免重复输出。我们的解决办法是每次输出时,将调用栈每行的行号都记录下来。下次输出时把当前的跟原先的比较,如果是一致,表示属于同一次操作,直接忽略掉。

  1. 记录传入的参数。这个很容易理解,比如调用 setText("hello") 时,把 hello 记录下来。

通过上述三步,我们就可以获得操作步骤的信息。有了这些信息后,需要在合适的位置调用日志函数,将步骤输出到日志中。在 Notes 项目使用的控件库中,所有 Notes 控件有一个基类,这个类定义了 click 方法,控件库的其它类都是由它继承而来。因为几乎所有的 UI 操作都会直接或间接地调用 click 方法,我们只需要在 click 函数开始的时候,组合上面说的四项信息成测试步骤,然后调用日志函数把测试步骤输出到日志文件中。在这里,我们给出的只是基本原理,您可以结合自己项目实际的特点,实现这一功能。

总结

提高测试结果分析效率和自动化测试过程同样重要。本章从测试用例的准备开始,由浅入深地介绍了如何优化测试用例,用例如何记录日志,以及日志能提供什么功能,各个功能又是如何实现。在最后,我们还介绍了自动记录测试步骤的基本原理。希望能够为您提供改进测试日志的思路和指导。在本系列的下一章,我们将会为读者介绍控件库的设计和使用,其中也会介绍录制用例和自动生成对象定义的基本原理,敬请期待。