1、1用 WinRunner 实现软件的全球化测试作者:月白(笔名) 甲骨文软件研究开发中心(北京)有限公司北京市海淀区中关村软件园孵化器 2 号楼 A 座一层 邮编:100094移动电话:13651010781, 邮件:摘要本文采用循序渐进的方法详细的介绍了如何用 WinRunner 实现软件的全球化测试。当然,单靠 WinRunner 本身是无法完全实现的,我们开发了一个小程序 COFAL 来帮助WinRunner 实现全球化测试。通过学习这篇文章,您可以掌握: WinRunner 的在全球化测试中的缺陷 WinRunner 本身可用于全球化测试的地方 COFAL 如何帮助 WinRunner
2、 实现全球化测试 COFAL 的实现细节关键字Globalization(g11n ) ,Internationalization(i18n) ,localization(l10n) ,Code Once Fit All Language(COFAL )1. 背景全球化已经成为当今软件发展的趋势,许多大型跨国软件公司都在亚洲设立了自己的专门从事全球化测试的部门。2004 年的 8 月,我加入 Oracle 甲骨文北京研发中心,正式成为这其中的一员,我测试的软件是 Oracle Application Server 10g,以下简称Oracle AS。 Oracle AS 是一个基于 J2EE
3、架构的应用系统,详细的介绍您可以参考OTN 上的相关文档。1.1 全球化中的概念全球化的英文是 Globalization,由于单词较长,所以为了书写方便,通常缩写为G11N,中间的 11 代表首字母”G”和尾字母”N”之间省略的 11 个字母。引用”中国本地化” 网站上对全球化的定义: Globalization 是使产品或软件进入全球市场而进行的有关的商务活动。包括正确的国际化设计,本地化集成,以及在全球市场进行的市场推广、销售和支持的全部过程。全球化中与我们测试直接相关的有国际化设计和本地化集成。国际化的英文是 internationlization,由于单词较长,通常缩写为 I18N,
4、中间的18 代表首字母”I”和尾字母”N”之间省略的 18 个字母。引用”中国本地化”网站上的定义:2国际化设计是指设计一个适用于多种语言和地区的应用程序的过程。适用于多种语言和地区的含义是当使用不同语言及处于不同的地区的用户在使用这个应用程序时,应用程序必须使用他们能看懂的语言和符合他们文化习惯来显示信息。本地化的英文是 localization,由于单词较长,通常缩写为 L10N,中间的 10 代表首字母”L”和尾字母”N”之间生罗的 10 个字母。引用”中国本地化 ”网站上的定义:本地化是指将产品或软件针对特定国际语言和文化进行加工,使之符合特定区域市场的过程。真正的本地化要考虑目标区域
5、市场的语言、文化、习俗和特性。通常包括改变软件的书写系统(输入法) 、键盘使用、字体、日期、时间和货币格式等。Locale表示表示一个特定的地理、政治或文化的区域,在java中有Locale类,我们会在1.3小节中给出详细的描述 1.2 全球化测试的内容简单的说,全球化测试主要是测试软件的处理数据和显示数据的功能。以 Oracle AS 为例: 处理不同的字符集(encoding)数据Oracle Internet Directory(简称 OID)是一个 LDAP 服务器,数据保存在Oracle 数据库中,现在想测试它创建用户的功能,要求用户的 DN 可以为不同国家的字符集,通俗的说,可以创
6、建英文的 DN,简体中文的 DN 和日文的 DN 等。当然具体可以创建哪些字符集的 DN 也要看当前 Oracle 数据库的字符集,只是那些在可以和当前字符集正确转换的字符集中的 DN 才可以正确的创建,否则很有可能无法创建或者创建的结果错误,如我们经常会看到的一些数据变成了问号(?) 。 动态显示与 Locale 有关的数据Oracle Delegated Administration Services(简称 DAS)是一个通过 web 页面访问的组件,页面的编码方式为 UTF8,要求当选择不同的浏览器语言时,以下各项都可以显示为与当前 Locale 相符的形式: 页上元素的文本类型的属性如
7、某个页的标题,在英文下为“Home” ,在中文下为“主页 ”;某个按钮上的标签,在英文下为“OK” ,在中文下为 “确定” 。 表示日期、时间、时区和货币等的文字如某个页上的一段表示出生日期的文字,在英文下显示为“January 1, 1976”,在中文下显示为“1976 年 1 月 1 日” 。1.3 Java 程序的国际化设计Java 语言是平台无关的,它采用双字节字符编码(UTF16 ) ,在解决国际化问题上有天生的优势。下面我要介绍的是 Java 中“动态显示与 Locale 有关的数据”的原理。这里要用到的几个主要类都在 java.util 包(package)中,包括有Locale
8、、ResourceBundle 、ListResourceBundle、PropertyResourceBundle等,其继承关系如下图所示:3 Locale该类包含对主要地理区域的地域化特征的封装。通过设定 Locale,我们可以为特定的国家或地区提供符合当地文化习惯的字体、符号、图标和表达格式。例如,我们可以通过获得特定 Locale 下的 Calendar 类的实例,显示符合特定表达格式的日期。Locale 有以下三个构造函数: Locale(String language) Locale(String language,String country) Locale(String lan
9、guage,String country,String variant)language 参数 :代表两个小写英文字符的 ISO 语言编码,如 zh 表示 Chinese,可用的语言编码可以参考:http:/www.ics.uci.edu/pub/ietf/http/related/iso639.txtcountry 参数 :代表两个大写英文字符的 ISO 国家或地区编码,如,CN 表示China, TW 表示 TAIWAN,国家代码对照表如下:http:/userpage.chemie.fu-berlin.de/diverse/doc/ISO_3166.htmlvariant 参数 :代表与
10、供应商或浏览器相关的代码。如,WIN 表示 windows,MAC 表示Macintosh,POSIX 表示 POSIX。当有两个 variant 存在的话,用下划线(uderscore)连接,并把最重要的 variant 放在前面。下面是几个典型的 Locale 的例子Locale(“ja“)Locale(“zh“,“CN“)Locale(“zh“,“TW“,“WIN“)Locale(“es“,“ES“Traditional_WIN“)Locale.getDefault(),得到当前 Java 虚拟机的宿主系统上默认的 Locale ResourceBundle该类是一个抽象类,它定义了三个
11、静态方法来获得具体的实现类(ListResourceBundle 的子类或 PropertyResourceBundle 类)的实例: static final ResourceBundle getBundle(String baseName)等同于调用:getBundle(baseName,Locale.getDefault(),this.getClass.getClassLoader()使用的是系统缺省的 Locale。 static final ResourceBundle getBundle(String 4baseName,Locale locale)等同于调用:getBundle(
12、baseName,locale,this.getClass.getClassLoader()使用的是参数 locale 指定的 Locale。 static final ResourceBundle getBundle(String baseName,Locale locale,ClassLoader loader)下面我们来说说 baseName 参数和 locale 参数。BaseName 参数 指定的是一组 ReourceBundle 的公共的基础名称,例如,设 baseName等于“TestBundle ”。如果用 ListResourceBundle 子类来实现,则要有如下这样的类:
13、TestBundle.class、 TestBundle_zh_CN.class 和 TestBundle_fr.class等;如果用 PropertyResourceBundle 来实现,则要有如下这样的属性文件:TestBundle.properties、 TestBundle_zh_CN.properties 和 TestBundle_fr.properties 等。locale 参数和选择策略一起决定运行时具体选择这组 ResourceBundle 中的哪一个。假设 locale 参数指定的 Locale 为(language1,country1,variant1) ,系统默认的 Lo
14、cale 为(language2,country2,variant2) ,则按照以下优先级的顺序查找最满足条件的 ResourceBundle: baseName + “_“ + language1 + “_“ + country1 + “_“ + variant1 baseName + “_“ + language1 + “_“ + country1 baseName + “_“ + language1 baseName + “_“ + language2 + “_“ + country2 + “_“ + variant2 baseName + “_“ + language2 + “_“ +
15、 country2 baseName + “_“ + language2 baseName 在每一种情况下,会先尝试按 ListResourceBundle 类的方式加载,失败后会再尝试按照访问属性文件的方式加载 PropertyResourceBundle 类。如果所有这些情况都没有找到的话最后会抛出一个MissingResourceException 的异常。注意,在第一个 getBundle 静态函数中 locale 参数指定的 Locale 就是系统默认的Locale。 ListResourceBundle该类继承 ResourceBundle 类,也是一个抽象类。它实现了 Resou
16、rceBundle 类中的抽象函数 getKeys()和 handleGetObject(String key),并提供了一个抽象函数getContents()。在应用中,通过创建继承 ListResourceBundle 的子类来实现ResourceBundle。要求子类必须实现 getContents 函数并提供一个包含有一组属性对的数组,如:package oracle.cdc.sgt.unicode;5import java.util.ListResourceBundle;public class MResources extends ListResourceBundle public
17、 Object getContents() return contents;static final Object contents = “s1“, “Home“;package oracle.cdc.sgt.unicode;import java.util.ListResourceBundle;public class MResources_zh_CN extends ListResourceBundle public Object getContents() return contents;static final Object contents = “s1“, “主页“ ;下面是一个 j
18、ava 类根据不同的 Locale 从相应的 ListResourceBundle 子类中取数据来显示:package oracle.cdc.sgt.unicode;import java.util.ResourceBundle;import java.util.Locale;public class TestListResourceBundle public static void main(String args) ResourceBundle messages;Locale curloc;try if (args.length != 2) curloc = Locale.getDefau
19、lt(); else curloc = new Locale(args0, args1);messages = ResourceBundle.getBundle(“oracle.cdc.sgt.unicode.MResources“, curloc);System.out.println(messages.getString(“s1“); catch (Exception e) System.out.println(e);把这三个类加入到 classpath 中,运行“java TestListResourceBundle zh 6CN”或在简体中文操作系统上运行“java TestListR
20、esourceBundle”,打印出“主页” ;运行“java TestListResourceBundle en”或在英文操作系统上运行“java TestListResourceBundle”,打印出“Home” 。 PropertyResourceBundle继承 ResourceBundle 类,它不是抽象类,也不需要创建它的子类。与ListResourceBundle 相同的是它也实现了 ResourceBundle 类的抽象函数getKeys()和 handleGetObject(String key);不同的是,它是从属性文件(.properties)中读入属性对的。例如,定义如
21、下一组 properties 文件,并加入到 classpath 中:MResources.properties:s1=HomeMResources_zh_CN.propertiess1=主页下面是一个 java 类根据不同的 locale 从相应的 Properties 文件中取数据来显示:package oracle.cdc.sgt.unicode;import java.util.Locale;import java.util.ResourceBundle;public class TestPropertyResourceBundle public static void main(St
22、ring args) ResourceBundle messages;Locale curloc;if (args.length != 2) curloc=Locale.getDefault(); else curloc = new Locale(args0,args1);messages = ResourceBundle.getBundle(“oracle/cdc/sgt/unicode/MResources“,curloc);System.out.println(messages.getString(“welcome“);运行的方式和结果同 TestListResourceBundle 一
23、样。留意一下 TestListResourceBunlde 和 TestPropertyResourceBundle 唯一不同的地方就是在调用 getBundle 函数的那个语句,按照我们上面所说的,完全可以统一写成:“oracle.cdc.sgt.unicode.MResources” ,因为 getBundle 会缺省先找基础名称为 MRseources 的类,失败后再找基础名称为 MResources 的属性文件,在查找属性文件前它会自动把“.”转换为“/ ”。当然,如果通过存在基础名称为 MResources 的类和属性文件时,也可以通过直接使用“oracle/cdc.sgt/unic
24、ode/MResources ”来略过查找基础名称为 MResources 的类。当然,java 程序的国际化设计并不只是这么简单,当涉及日期和时间显示等问题时,还7可以利用 java.text 包以及 java.util 包中的 TimeZone、SimpleTimeZone 和Calendar 等类进行辅助处理。我们就不在这里详细叙述了,您只需要记住一个ResourceBundle 的概念就可以了,本文的后续部分都是围绕着这个概念展开的。2. WinRunner 调研WinRunner 适合于测试那些有图形操作界面的组件。目前,我们手头可用的版本WinRunner7.5,启用 Web 和
25、Java 插件(plugin) 。让我们先从 WinRunner 的技术特点说起吧。2.1 WinRunner 的技术特征由于本文不是专门介绍 WinRunner 的,所以只列举一些 WinRunner 的重要特征。注意:这里定义了一些非官方的术语,为的是便于您的理解。 WinRunner 将对象(object)分为两种:窗口(window )和子对象,任一个子对象都隶属于一个窗口。注意:窗口也是对象,如一个页面就是一个窗口。 WinRunner 通过一组属性来唯一的识别窗口,也就是说不能有所有属性值都相同的多个窗口;同样的,WinRunner 通过一组属性来唯一的识别同一个窗口下的子对象,也
26、就是说,在同一个窗口下,不能有所有属性值都相同的多个子对象。 如果把对象的所有属性的集合称为对象的定义,WinRunner 可以把对象的定义保存在以下两个地方: 独立 script 的单独的扩展名为 GUI 的文件简称为 GUI 文件,同时为每个 object 定义了一个绝对逻辑名,有了绝对逻辑名就一定有相对逻辑名。对于窗口来说,它的绝对逻辑名等于它的相对逻辑名;对于子对象,它的绝对逻辑名等于它隶属的窗口的绝对逻辑名后面加一个”.”再加上它的相对逻辑名。在 script 开始部分导入 GUI 文件,在后面部分中只需要写出对象的绝对逻辑名,就可以从 GUI 文件中获得这个对象的定义了。如:“Or
27、acleAS Certificate Authority-Certificate Management“:class: window,MSW_class: html_frame,html_name: “!OracleAS Certificate Authority-Certificate Mana.*“ltree_state: open,list_open_data: close“OracleAS Certificate Authority-Certificate Management“.“Advanced Search“:8class: object,MSW_class: html_text
28、_link,html_name: “Advanced Search“OracleAS Certificate Authority-Edit Policy Result: UniqueCertificateCo“:class: window,MSW_class: html_frame,html_name: “!OracleAS Certificate Authority-Edit Policy Resu.*“rtree_state: open,ltree_state: open,list_open_data: close script 本身把 object 定义写在 script 是可以的。一种
29、方法是象 GUI 文件那样在 script 的开始部分为对象定义一个绝对逻辑名,这样在 script 的后续部分就可以通过这个绝对逻辑名来访问对象的;另一种方法是不为对象定义绝对逻辑名,而是在每个要访问对象的地方,直接写该对象的定义。这两种在 script 中定义对象的方法我们都不推荐,第一种完全就是 GUI 文件在 script 中的实现,那么为什么不它放在 GUI 文件中统一管理呢,第二种虽然省略了导入 GUI 文件的一步,但是维护起来更麻烦了,如果对象的属性发生变化,那就要修改所有脚本中所有这样定义了该对象的地方。也许我们还没有意识到在 script 中定义对象的好处,但是存在就是道理。
30、如:#Gui Objects initializationset_window(“ class: window, MSW_class: html_frame, html_name: “OracleAS Wireless Tools“,151);list_select_item(“ class: list, MSW_class: html_combobox, html_name: matchType“,“Matches“);rc=global_web_obj_text_exists(access_info,“Wireless and Voice Settings“);if(text!=“Appl
31、ication Links“)set_window(“ class: window, MSW_class: html_frame, html_name: “Oracle Enterprise Manager - Notification Event Collector: “rc=global_web_obj_text_exists(text_object,“#1“,“#1“,“Access 9Information“,“,“);set_window(“ class: window, MSW_class: html_frame, html_name: “OracleAS Wireless Too
32、ls“,151);2.2 WinRunner 在全球化测试中的局限性在 1.1“全球化测试的内容”一节中我们知道,要在不同的 Locale 下测试软件的处理数据和显示数据的能力。在不同 Locale 下,WinRunner 赖以识别对象的属性列中有的属性也可能不同,因此在不同的 Locale 下,同一个对象的定义也可能不同。如同一个窗口,在英文下的 html_name 属性值为 “OracleAS Certificate Authority-Certificate Management”,在简体中文下的 html_name 属性值为 “OracleAS Certificate Authorit
33、y-证书管理” 。也就是如果用该窗口在英文下的定义是无法在简体中文或其他 Locale 下识别该窗口的。换句话说,在一种 Locale 下录制的脚本,不论对象定义是保存在 GUI 文件中还是保存在script 中,都无法直接拿到另一种 Locale 下直接运行。注意:所谓直接拿到,也包括进行少量的修改。虽然可以在不同的 Locale 下用 WinRunner 录制各自的脚本,但是这并不是我们所希望的,那样做的成本是非常高的。我们的目标是只在一种 Locale 下录制脚本,经过一定处理后,就可以在其他 Locale 下使用,即 Code Once Fit All Language(简称 COFA
34、L)。2.3 WinRunner 满足 COFAL 的技术可行性既然 WinRunner 是通过对象的定义(一组属性) 来标识对象的,那么我们就要研究对象的属性在不同的 Locale 下有什么不同: 有的对象在不同的 Locale 下所有属性值都不变 有的对象在不同的 Locale 下部分属性发生变化只要找到变化的属性的规律和属性值的来源,并用自动化的方法来修改这些属性,就可以基本上满足只录制一次的需求。1、如果对象的定义保存在 GUI 文件中假如在英文下录制了一套脚本,该套脚本公用一个 GUI 文件 global.gui,我们要找到一个自动化的方法,生成该 GUI 文件在其他 Locale
35、下对应的 GUI 文件,如global_zh_CN.gui 和 global_fr.gui 等。这样,在不同 Locale 下,通过使用不同的 GUI 文件就可以用同一套脚本运行了。这样看来虽然有多个 GUI 文件,但是脚本只有一套,其他的 GUI 文件又是自动生成的,基本上满足了 COFAL 的要求。2、如果对象的定义保存在 script 中假如在英文下录制了一套脚本,脚本都保存在 tina 目录下,对象的定义都保存在script 中。我们要找到一个自动化的方法,转化该 script 中对象的定义到不同的Locale 下的定义,并把转化的结果保存在新的目录下,如 tina_zh_CN 和 t
36、ina_fr 目10录等。这样,在不同的 Locale 下,通过使用不同目录下的脚本就可以了。这样看来虽然有多套脚本,但是只录制了一次,其他的都是自动生成的,也基本上满足了 COFAL 的要求。下面我们会把转化对象定义统一称为“翻译” ,接下来要介绍的就是以 COFAL 命名的一个实现自动化翻译的小工具。3. 自动翻译工具 COFAL 简介CORAL 是 Code Once Fit All Language 的缩写,它是专门为配合 WinRunner 的全球化测试而开发一个工具,code once fit all language 的意思是只需要在一种语言下编写脚本,就可以在所有语言下运行。用
37、 COFAL 来实现 script 和 GUI 文件的自动翻译。3.1 技术原理3.1.1 Java 应用程序级数据翻译Oracle AS 是一个基于 J2EE 架构的应用程序,它是通过我们在 1.2 节中介绍的 java数据绑定机制来实现国际化的,也就是说那些需要翻译的属性值其实都是保存在ResourceBundle 中。除了前面说过的 ListResourceBundle 和PropertyResourceBundle 外,Oracle AS 还把部分 ResourceBundle 保存在数据库的表中,在运行时根据不同的语言环境用绑定的 key 动态的从表中检索出对应的值。由此可知,程序本
38、身是通过关键字(key)结合指定的 locale 来获得值(value)的;而现在是想在已知值(value) 和指定的 locale 的情况下,获得该 key 在其他 locale 下的值(value),这是一个逆向的过程。为了描述方便,我们用 key 代表关键字,用 prevalue 代表当前可得的值,用postvalue 代表翻译后的值。自动翻译的原理是:(1) 事先定位好 resourcebundle 的保存位置。(2) 翻译时,从 script 或 GUI 文件中提取出 prevalue,然后用 prevalue 在ResourceBundle 中查询出 key,再用 key 从 ResourceBundle 中查询出postvalue。(3) 用 postvalue 替换 prevlaue,把替换的结果保存成新的语言版本。在 COFAL 中,我们把 Oracle AS 用到的 ResourceBundle 统一保存在一张 Oracle 数据库表 GLOBALRES 中。翻译时通过 JDBC,用 prevalue 从表中 select 出 key,再用key 去 select 出 postvalue。