IBM® Rational® Functional Tester 是自动化功能测试和回归测试工具 , 为功能测试、回归测试、GUI 测试和数据驱动测试提供了自动化测试功能。Rational Functional Tester 的功能强大,但在具体使用过程中仍然需要掌握必要的技巧并针对特定的被测试应用定制测试的框架。如何使用 Rational Functional Tester 高效开发出满足测试用例需求的,健壮的和可维护的自动化脚本是实际项目中的挑战之一。而如何应对和解决该挑战是本文的主要目的。本文的全部内容来源于具体工作的实践。我们的项目要求在较短的时间内为一个较复杂的在线采购类应用实现其核心回归测试用例库(400 多测试用例 ) 的自动化脚本。本文将介绍在创建在线采购类应用自动化测试框架和测试脚本中遇到的一些实际问题和相应的解决方法,以帮助您更好的使用 Rational Functional Tester 实现 Web 应用的自动化测试。
先决条件
为了在短时间内迅速了解该应用的业务逻辑规则,项目经理作为脚本执行人员首先挑选适量不同属性的测试用例手动运行并迅速掌握,方便扫清自动化测试人员在理解测试脚本遇见的障碍,并且客观判断每个验证点的正确性。
首先是最底层的测试对象层(Object 文件夹),本文对 ObjectRepository 文件根据不同页面或者业务逻辑进行封装,保证上层能够迅速准确地找到 GUI 对象并操作;其次中间层是通用方法层(Task 文件夹),分通用方法和定制化方法的封装,实施对底层的操作并向上层提供接口。其中 Common Task 文件是对通用控件的操作,例如文本框的文本设置,按钮的点击,链接的双击,等等。Custom Task 文件中的方法通常是对特殊页面的操作,例如点击某链接后的编辑,获取通用对象的属性等等;最上层是测试用例层(Test Case 文件夹),读取 XML 数据文件及其他配置文件执行测试步骤,验证测试结果并输出测试执行日志。
对于常用控件的操作结果不能体现在 Log 里,即 Log 里不记录某一步点击或者赋值操作的成功失败,当执行时遇见 Exception 后,无法快速准确定位出错原因,即不方便检查是在操作哪个控件时报错。
首先根据 ObjectRepository 中的属性新建控件,然后调用 loggers/VisualReporter/logScriptInfo 方法记录操作结果。同时为了精确,控件名称取自控件属性的 desc,方便测试员调试。
public void clickButton(String objProps) { WButton button=of.getwButtonField(objProps); wlc.loggerActionForButton(button, of.getDescription()); }
在线采购类 web 应用通常有数量庞大的导出报告类测试用例。常用方法是将期望结果分行或者分列拼成完整的字符串写在数据验证的 XML 文件里。但是当数据量非常庞大时此方法不适用,因为会无形中翻倍测试人员的工作量。当文件格式不同时需要写不同文件格式的比较方法;起止的行列都有不同;每个期望的 report 出现在 Excel 文件的不同 Tab 或者 Section 里;有些 cell 里是时间戳无法直接比较;有些 cell 里是相关人员的联系信息在不同系统里显示不一致(例如张三用户在 115 测试系统里手机号是 135,而在 211 脚本验证系统里手机号是 138),同一个 section 下每行出现的顺序不同,例如第一次运行后显示 1-2-3-4-5 行,第二次运行后显示 2-3-1-5-4 行(开发不规范导致),等等类似问题在验证导出报告时层出不穷。
我们在这种情况下用到的改进方法是,直接比较 Expected Excel Report(作为模板)和 Export Excel Report。当 Report 格式发生变化时,例如 xlsx, xls, csv 只需要稍微修改底层的调用方法即可;起止行和起止列不定时,都当做 index 参数传入方法里;Excel report 的 tab/sheet 不定时,也作为参数传入,避免整个文件比较浪费时间;时间戳无法直接比对时,将时间戳按照指定格式模糊匹配后替换,例如 Report Date::xx/xx/xxxx xx:xx:xx;当有特殊信息在不同系统显示不同时,我们 Exclude 当前列转而比较其他可做比较的有意义列;当每行出现顺序不一致时,我们以每行的字符串数据作为一个小单元,比较其是否包含在期望报告的行列中。
public void verifyExcelReport_MixedMatch_ExcludedCols (String sSectionName, String sTempateFileName, String sExportedFileName, int iSheet, int iStartRow, int iEndRow, int iStartCol, int iEndCol, String sExcludedCols){ String sVPName = "Verify Report contents of section: " + sSectionName + ", file: " + sExportedFileName; String sActual = ""; String sOriginal = ""; if (sTempateFileName.endsWith("xls")){//.xls 格式的文件比较 sOriginal = importdata.getExcelSectionContents_ExcludedCols_XLS(sTempateFileName, iSheet, iStartRow, iEndRow, iStartCol, iEndCol, sExcludedCols); // 返回期望报告指定行列的字符串 sActual = importdata.getExcelSectionContents_ExcludedCols_XLS(sExportedFileName, iSheet, iStartRow, iEndRow, iStartCol, iEndCol, sExcludedCols); // 返回实际导出报告的指定行列的字符串 }else{//.xlsx 格式的文件比较 sOriginal = importdata.getExcelSectionContents_ExcludedCols_XLSX(sTempateFileName, iSheet, iStartRow, iEndRow, iStartCol, iEndCol, sExcludedCols); sActual = importdata.getExcelSectionContents_ExcludedCols_XLSX(sExportedFileName, iSheet, iStartRow, iEndRow, iStartCol, iEndCol, sExcludedCols); } // 比较期望报告和实际报告字符串的相等关系 logger.containedAndLogVP_SectionMatching(sVPName, sOriginal, sActual, "###"); }
public String getExcelSectionContents_ExcludedCols_XLS(String sFileName, int iSheetIndex, int iStartRowIndex, int iEndRowIndex, int iStartCol, int iEndCol, String sExcludedCols){ String sColsNotNeeded = "#" + sExcludedCols.trim() + "#"; String sMultipleRowContents = ""; try{ // System.out.println("filename: " + filename); FileInputStream file = new FileInputStream(new File(ObjectRepository.TestDataPath+sFileName)); HSSFWorkbook workbook = new HSSFWorkbook(file); HSSFSheet sheet = workbook.getSheetAt(iSheetIndex); for (int i = iStartRowIndex; i <= iEndRowIndex; i++){ HSSFRow row = sheet.getRow(i); String sSingleRowContents = ""; if (row != null){ // System.out.println("not null"); for (int j = iStartCol; j <= iEndCol; j++){ if (!sColsNotNeeded.contains("#" + j + "#")){ HSSFCell cellCurrent = row.getCell(j); //System.out.println(getExcelCellText(j)); sSingleRowContents = sSingleRowContents + ":" + getExcelCellText_xls(sFileName, iSheetIndex, i, j); } } // End for sSingleRowContents = sSingleRowContents.replaceFirst(":", ""); sMultipleRowContents = sMultipleRowContents + "###" + sSingleRowContents; } // End if } // End for String sDateTime_String = "//d{2}" + "/" + "//d{2}" + "/" + "//d{4}" + " " + "//d{2}" + ":" + "//d{2}" + ":" + "//d{2}"; sMultipleRowContents = sMultipleRowContents.replaceAll(sDateTime_String, "xx/xx/xxxx" + " xx:xx:xx"); sDateTime_String = "//d{4}" + "/" + "//d{2}" + "/" + "//d{2}" + " " + "//d{2}" + ":" + "//d{2}" + ":" + "//d{2}"; sMultipleRowContents = sMultipleRowContents.replaceAll(sDateTime_String, "xxxx/xx/xx" + " xx:xx:xx"); System.out.println("full content: " + sMultipleRowContents); file.close(); }catch(FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return sMultipleRowContents; }
被抓取的控件属性在通常情况下都封装在 ObjectRepository 的文件里,在操作控件时直接调用该文件中的某个控件。但是随着逻辑复杂和页面增多,单个文件中控件积累上万行代码,RFT 搜索会明显变慢;多个自动化测试员编辑又会经常产生彼此覆盖,命名重复的问题。由于开发不规范等原因,控件的属性值中被人为引入乱码,例如 OK 按钮的属性 .value=Â Â OKÂ Â,或者干脆是随机数,这种情况下变动的属性值记入 ObjectRepository 文件中也会影响下次的脚本回放。
为了解决单个文件存储过多控件的问题,在项目中我们将 ObjectRepository 按照页面 UI 分开,以避免搜索和编辑问题。为了解决控件开发不规范的乱码和随机数问题,首先用 Regex_ 来模糊匹配控件属性值以避开乱码问题,如下清单所示。其次调用 RegularExpression 方法,根据随机数的生成规则来模糊匹配控件,具体见以下清单所示。
public static String ItemLink_ItemName(String itemName){ return "desc=Item Name,.class=Html.A,.text=Regex_/""+itemName+"/""; }
// Pattern to match Lot/Item column RegularExpression re_ColItem= new RegularExpression( "TD_" + "//d*" + "_0$", false); // Pattern to match Status column RegularExpression re_ColData= new RegularExpression( "TD_" + "//d*" + "_" + sCol_Index + "$", false); TestObject[] itemList = getBiddingInfo.returnDIVList(oParent, re_ColItem); TestObject[] divListStatus = getBiddingInfo.returnDIVList(oParent, re_ColData);
在回归测试用例中,经常会遇见测试数据是最长字符串并且是全乱码,常用的文本设置方法不能直接使用。当乱码在数据文件中,而数据文件又恰巧不是 excel 格式而是 XML 格式,取出数据有误。当需要将乱码设置给某个文本控件时,常用的文本设置方法也不适用。
为了解决数据文件中的乱码问题,我们使用了 XML 转义的方法,即下表所示,当遇见小于号,大于号,和,单引号以及双引号时,将其替换为规定的字符,即可顺利地从 XML 数据文件中获取正确的特殊字符。为了解决某文本控件的特殊字符设置问题,需要先将特殊字符拷贝在粘贴板上,然后通过键盘操作 Ctrl+V 的粘贴行为将特殊字符输入文本框。这种方法简单快速有效地解决了乱码字符设置问题。
# | 名称 | 符号 | 转义后符号 |
---|---|---|---|
1 | 小于号 | < | < |
2 | 大于号 | > | > |
3 | 和 | & | & |
4 | 单引号 | ‘ | ' |
5 | 双引号 | “ | " |
<rfxUniqueID> RA_LWR_Report ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_ +{}|:<>?/.,; ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_+{}|:<>?/.,; ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_ +{}|:<>?/.,; ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_+{}|:<>?/.,; ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_+{}|:<> ?/.,; ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^& </rfxUniqueID> <item1NewName>Item001 ¥£¢§ ©¶ µ ß @#!$%^&*()~ -@#%^&*()_+{}|:<> ?/.,;'[]~ ¥£¢§ ©¶ ¥£¢§ ©¶ µ ß @#!$%^&*()~ </item1NewName> 清单 7. 特殊字符的文本设置方法 public void setTextThruClipboard(String s) { //get properties need for logging before you click,// //because otherwise click might take you to another// // page causing an ObjectNotFoundException // String sWidgetType = getWidgetType(); String sWidgetName = getName(); this.clearText(); //set clipboard content with specified string this.setClipboardText(s); //paste clipboard contents this.typeKeys("^v"); PackageLoggingController. logPackageInfo(PackageLoggingController.PACKAGELOGLEVEL_ALL_WIDGET_SETTERS, "Set text thru clipboard /"" + s + "/" in " + sWidgetType + " /"" + sWidgetName + "/""); }
如下图显示,根据业务需求,Multiplier(A) Item 不允许被定义为零。如果用户输入了零,将会得到以下错误提示:
The value you entered for amount is invalid. Please enter a value greater than 0.00000000
测试用例要求对错误信息的内容是否正确进行验证。
由图中可以看到,这个错误信息会以 Tooltip 的形式显示出来。这种形式的提示信息的特点在于,在用户将鼠标从出错区域移走之后,将会被隐藏;只有鼠标放在出错区域内错误信息才会显示。
在尝试用 RFT 测试对象检查器来对错误信息进行识别后,我们发现,目前,RFT 对于这种会随鼠标的转移而隐藏的 Object, 是无法进行录制识别的,
针对上述问题,接下来,我们尝试通过 RFT 测试对象检查器来识别与这个 Tooltip 错误信息同一层次的警告图标控件,即图 4 中显示的黄色小图标。下图是用 RFT 测试对象检查器对它进行识别的结果。其 .class 属性为 Html.IMG。
由于根据一般的开发习惯,我们推测这个警告图标应该是和我们所要验证的错误信息同属一个父亲,因此,我们向上查找了所识别出来的这个警告图标的父亲,如图 6 所示 , 我们找到了警告图标的父亲控件,其 .class 属性为 Html.FRAME,其 .name 属性为 bidAttr_list。
需要说明的是,在我们的这个例子中,我们从错误图标开始向上找了多个层次,最终,选定了 .class 属性为 Html.FRAME,.name 属性为 bidAttr_list 的父亲作为接下来操作的对象。这样选择的目的,是为了尽可能扩大查找范围,保证我们真正要查找的 Tooltip 错误信息控件能在这个父亲下面找到。在实际应用中,大家可以根据项目情况,自行决定扩大或缩小查找范围,只需提高或降低距离错误图标的父亲控件所在的层次即可。比如,如果想要进一步扩大查找范围,可以选择更上一层的 .class 属性为 Html.FRAME,.name 属性为 right 的父亲作为接下来操作的对象。如图 7 所示。
在确定了查找范围后,我们可以利用 find 方法和 getProperties 方法来查找我们所选定的 .class 属性为 Html.FRAME,.name 属性为 bidAttr_list 的父亲对象下面是否有我们想要查找的 Tooltip 对象。
在查找 Tooltip 的时候,我们选择了查找属性为 Html.DIV 的所有对象,原因是,对于这个在线拍卖应用产品,开发工程师在编写代码的时候,一般会讲错误信息属性定义为 Html.DIV。
代码清单 8 显示了定位 .class 属性为 Html.FRAME,.name 属性为 bidAttr_list 的父亲对象,查找父亲对象下面属性为 Html.DIV 的所有对象,以及打印属性为 Html.DIV 所有对象的所以属性值的过程。
// 查找 .class 属性为 Html.FRAME,.name 属性为 bidAttr_list 的父亲对象 TestObject to[] = getRootTestObject().find(atDescendant(ObjectProperty.gsClassProp, "Html.FRAME", ObjectProperty.gsNameProp, "bidAttr_list")); System.out.println("to.length="+to.length); // 在父亲对象下面查找属性为 Html.DIV 的对象 TestObject divTos[] =to[0].find(atList((atDescendant(ObjectProperty.gsClassProp, ObjectProperty.gsHtmlDIVRef)), atChild(ObjectProperty.gsClassProp, ObjectProperty.gsHtmlDIVRef)),false); // 打印出所有属性为 Html.DIV 对象的所有属性值 System.out.println("divTos[] find done,and length="+ divTos.length); for (int i = 0; i < divTos.length; i++){ System.out.println("============i="+i); System.out.println("============divTos[i].getProperties ="+divTos[i].getProperties() }
代码清单 8 的打印结果如下所示:
============i=0 ============divTos[i].getProperties={...,.className =CHARCOUNTER, ...,.contentText=, ...} ============i=1 ============divTos[i].getProperties={...,.className= dijitTooltipConnector, ...,.contentText=, ...} ============i=2 ============divTos[i].getProperties={..., .className= dijitTooltipContainer dijitTooltipContents, ...,. contentText=The value you entered for amount is invalid. Please enter a value greater than 0.00000000, ...}
由于打印结果中的信息太多,我们去掉了我们这个举例中不关心的属性信息,仅保留了我们所关心的 .className 属性和 .contentText 属性。
通过清单 9 中的打印结果可以看到,在父亲对象下面总共找到了 3 个属性为 Html.DIV 的对象。其中,第三个对象(即 i=2)的 .contentText 属性和我们要查找的 Tooltip 的错误信息内容是完全一致的,其 .className 属性为“dijitTooltipContainer dijitTooltipContents”,和其他两个对象的 .className 属性也互不相同。因此,我们就可以利用 .className 属性和 .contentText 属性来匹配查找我们需要的 Tooltip 信息了。
至此,我们已经找到了我们需要的对象及其属性信息。接下来,我们将利用这些信息来编写一个方法来实现对 Tooltip 信息的验证。如代码清单 10 所示。
public void verifyToolTip_ErrMessage(String sVPName, String sExpected){ System.out.println("=============verifyToolTip_ErrMessagein================="); sVPName = "Veirfy that expected err message is display "; String sActual = ""; // 查找 .class 属性为 Html.FRAME,.name 属性为 bidAttr_list 的父亲对象 TestObject to[] = getRootTestObject().find(atDescendant(ObjectProperty.gsClassProp, "Html.FRAME", ObjectProperty.gsNameProp, "bidAttr_list")); // 在父亲对象下面查找属性为 Html.DIV 的对象 TestObject divTos[] =to[0].find(atList((atDescendant(ObjectProperty.gsClassProp, ObjectProperty.gsHtmlDIVRef)), atChild(ObjectProperty.gsClassProp, ObjectProperty.gsHtmlDIVRef)),false); // 打印出所有属性为 Html.DIV 对象的所有属性值 System.out.println("divTos[] find done,and length="+ divTos.length); for (int i = 0; i < divTos.length; i++){ System.out.println("============i="+i); System.out.println("============divTos[i].getProperty(.className) ="+divTos[i].getProperty(".className")); // 如果 .className 属性为"dijitTooltipContainer dijitTooltipContents",取得其 .contentText 属性值,并与期望值做比较 if (divTos[i].getProperty(".className").equals( "dijitTooltipContainer dijitTooltipContents")) { sActual = divTos[i].getProperty(".contentText").toString(); System.out.println(divTos[i].getProperty(".contentText").toString()); break; } } wlc.compareStringAndLogVP(sVPName, sExpected, sActual); }
在文本设置时,比如用户密码,比如较长的字符串,RFT 回放时经常会遇见设置字符数不够等问题导致登陆失败或者名称设置错误导致验证点失败。由于开发的不规范,Session 时长的设置有误。例如某个场景需要 90 分钟的各种设置及保存,期间会弹出若干个对话框的操作。而开发的 session 时长只是计算最底层页面的操作时间。也就是说,回放期间经常会出现 session 过期而导致的操作失败,而重新运行数小时的测试用例无疑是浪费时间而且低效的。有时 RFT 的操作不够精确会导致该切换的页面没有切换过去,而切换到了错的页面上,虽然发生概率非常小,但也给自动化测试人员带来困扰。综上问题,由于自动化测试开发周期短,任务重,根据这些问题给开发人员沟通,开 defect 并解决会严重延迟最终交付物的交付时间。
为了解决常用字符串设置失败这类问题,经过多次试验并优化后,当设置次数为 10 次时可以准确设置文本通用字符串,如下列清单所示。
为了解决 session 过期问题,本文作者采取了主动登出后再次登入继续操作的方法,简单有效地避免了过期问题。同样地,控件点错也可用主动登出的方法解决。
public void loggerActionForTextField(WTextField t, String sTextFieldName, String sInputText) { try { t.waitForExistence(MAX_WAIT, 3); t.setText(sInputText); // 平均设置 10 次文本值以确保设置值正确 int iTimes = 0; while (!(t.getText().equals(sInputText)) && iTimes < 10) { iTimes = iTimes + 1; t.clearText(); t.setText(sInputText); if (t.getText().equals(sInputText)) { break; } } //t.unregister(); log.logScriptInfo("Enter text /"" + sInputText + "/" in TextField /"" + sTextFieldName + "/" ", VisualReporter.LOGTYPE_TIME_PASS_FAIL); } catch (Exception error) { log.errorHandler("FAIL - Can not find input TextField /"" + sTextFieldName + "/" in current window "); } }
本文结合实际大型在线采购类应用的自动化测试阶段遇见的问题,介绍了常见的六大类问题及其在当前应用下最优的解决方式。经过本文中各种解决方法的实行,试验表明不仅回放时通过率高,而且客户在做脚本审查和在新环境运行时几乎不会出现相关问题的报错信息,脚本健壮且更容易在短时间内一次执行通过。因此可以说,上述疑难问题的解决方法是快速而有效的。
本文仅代表作者本人观点