翻译验证性测试是全球化测试中的一部分,旨在验证软件用户界面中需要翻译的字符串是否已经正确翻译。为了提高效率,目前一般采用先由熟悉产品的测试支持工程师书写测试用例,再由熟悉各国语言的测试人员执行用例或者先由测试支持工程师将复杂用例的执行结果在各个语言环境下截图,然后再发送给测试人员进行验证两种模式。目前随着持续交付模式的出现,无论是测试人员多语言环境下重复执行用例还是测试支持工程师多语言环境下手动截图都难以满足持续交付对速度的要求,因此测试支持工程师编写自动化脚本完成多语言环境下的自动截图就变成了提高效率的必然选择。
在传统的 Web 自动化测试应用场景中,由于程序编码的不规范,元素通常很难直接通过静态 ID 或 name 定位,幸好有 CSS 选择器和 XPath 定位为我们提供了新的可能。然而由于 DHTML 和 Ajax 的盛行,新的挑战又摆在了测试人员面前,由 Dojo 等客户端脚本生成的 DOM 结构异常复杂:层次繁多且一个 DOM 树上可能存在多个页面的 DOM 子节点,而且其中元素的 ID 是动态的(该 ID 一般由一个前缀和一个动态的数字组成,前缀表明了元素对应的部件类型,数字则由运行时该小部件在 DOM 树上的位置确定)。这导致了有些元素甚至需要依靠 CSS 伪类再运用组合选择器才能精确定位的到。而对于在翻译验证性测试中的自动化,这种问题尤其突出,由于翻译验证性测试中的自动化脚本“一次编写,多个语言环境下”运行的要求,元素定位的最后“救命稻草”-CSS 伪类中的内文本匹配也无法开箱即用了。
下面笔者将通过实例详细阐述元素定位的方法,以期读者能灵活运用以应对上述的各种挑战。
Selenium 是一款强大的浏览器自动化工具,利用它可以实现 Web 界面测试的自动化。根据实现原理和使用方式的不同,其又分为 Selenium IDE 和 WebDriver,前者以浏览器插件的方式赋予用户简单快速地创建自动化脚本的能力,后者则通过 API 库的形式向用户提供更加灵活和健壮的 Web 自动化能力。
Selenium IDE 在其命令中采用 locatorType=location 的格式进行定位,该方法将定位到条件匹配的第一个元素,一般情况下 locatorType 可以省略;WebDriver 利用 findElement(By.locatorType("value") 或 findElements(By.locatorType("value") 函数查找元素,前者将返回一个唯一的元素,后者将返回一个元素列表。具体的定位由 By.locatorType("value") 实现。
除去定位使用的方法而言,二者本质上都是通过 locatorType 进行定位的,常用的 locatorType 包括 id,name,link text,dom/JavaScript,xpath,css 等,示例见清单 2(行末括号中的数字显示当前行定位器将定位到清单 1 HTML 文档中的第几行元素):
//by IDid=loginForm(03)//in IDEWebElement element = driver.findElement(By.id("loginForm"));(03)//in WebDriver//by Namename=continue type=button(07)//in IDEWebElement element = driver.findElement(By.name("loginForm"));(03)//in WebDriver//by link textlink=Continue(10)//in IDEWebElement element = driver.findElement(By.linkText("Continue"));(10)//in WebDriver//by DOM or JavaScriptdom=document.forms[0].elements['username'](04)//use DOM in IDEWebElement element = (WebElement) ((JavascriptExecutor)driver). executeScript("return $('.required')[0]"); (04)//use JQuery in WebDriver //by XPath//form[@id='loginForm']/input[1](04)//by IDEWebElement element = driver.findElement(By.xpath("//input[@name='username']"));(04)//by WebDriver//by CSScss=#loginForm input[type="password"](05)//by IDEWebElement e = driver.findElement(By.cssSelector("input.passfield")(05)//by WebDriver
通过 ID 和 name 定位是最高效也是首选的定位方式,不过由于 name 不一定唯一,在定位时匹配条件的元素可能有多个,因此这种情况下只会定位到匹配条件的第一个元素。针对多个元素具有相同 name(或链接文本)属性的情况还需额外增加其他的过滤器才能进行精确定位,如清单 2 中 IDE 利用 name 进行定位的示例。
DOM 代表了整个 HTML 文档的结构,使用 JavaScript 可以访问 DOM 中的节点。Selenium IDE 基于 DOM 结构可以使用 JavaScript 的点操作符进行层次定位以简化定位操作,由于只有 DOM 定位器以“document”开头,因此“dom=”也可以省略;此外在 WebDriver 中甚至可以通过执行任意的返回值为 DOM 对象的 JavaScript 语句来查找元素,如清单 2 中 WebDriver 使用 JQuery(一种 JavaScript 框架)的元素查找函数$进行定位。
XPath 最初是用来在 XML 文档中定位 DOM 节点的语言,由于 HTML 也可以算作 XML 的一种实现,所以 Selenium 也可以利用 XPath 这一强大的语言来定位 Web 元素。XPath 在传统属性定位之外扩展了诸如“定位第三个多选框”等定位能力,以便应对没有 ID 或 name 属性的情况。利用 Xpath 可以通过绝对路径,或者相对于一个可精确定位的元素的相对路径来定位。为了保证定位的健壮性,推荐使用相对路径和基于位置关系的定位。同样由于只有 XPath 定位器以“//”开头,所以“xpath=”也可以省略。
CSS (Cascading Style Sheets) 是一种用于渲染 HTML 或者 XML 文档的语言,CSS 利用其选择器可以将样式属性绑定到文档中的指定元素,即前端开发人员可以利用 CSS 设定页面上每一个元素的样式。所以理论上说无论一个元素定位有多复杂,既然开发人员能够定位到并设置样式,那么测试人员同样应该也能定位继而操作该元素。这也正是 Selenium 官方极力推荐使用 CSS 定位,而不是 XPath 定位的主要原因。CSS 定位被推崇的另一个原因是不同的浏览器 XPath 引擎不同甚至没有自己的 Xpath 引擎,这就导致了 XPath 定位速度较慢,而采用 CSS 定位往往能用更简洁的语法快速定位到复杂的元素。因此后文将详细介绍 CSS 定位器的使用方法。
除了以上通用的定位方式之外,在 Selenium IDE 中还可通过 identifier 进行定位,这是最通用也是默认的定位方式,其定位时首先将 identifier 的值视为 ID 继而查找匹配元素,没有匹配项之后再把其看作 name 进行查找,直到找到第一个匹配的元素为止。由于其是默认的定位方式,因此“identifier=”通常也会省略。对于 WebDriver 而言,特有的定位方式更多。首先它可以通过链接文本进行部分匹配,如清单 3 所示;然后它还可以通过标签名或者 CSS 类名的方式进行定位,利用 findElement 的话这两种方式均是定位到第一个匹配条件的元素,利用 findElements 则可获得一个所有满足条件的元素列表,如清单 3 所示。
//by Partial Link TextWebElement element = driver.findElement(By.partialLinkText("Con")); (10)//in WebDriver//by Tag NameWebElement element = driver.findElement(By.tagName("p")); (09)/in WebDriver//by ClassListWEs = driver.findElements(By.className("required"));(4&5)//in WebDriver
CSS 选择器是由关系符分开的一个或多个简单选择器序列组成的选择器序列链,最后一个简单选择器序列的后面还可以添加一个伪元素。简单选择器序列是单纯由简单选择器组成的选择器链,它通常由一个标签选择器或者通配符选择器开头,之后序列中不允许再出现其他标签选择器和通配符选择器。简单选择器可以是 ID 选择器、类选择器、类型(标签)选择器、通配符选择器、属性选择器和伪类选择器。关系符可以是空格、大于号、加号和波浪符。关系符和简单选择器周围允许出现空白(空格符、tab 符、换行符、回车符、换页符)。多个 CSS 选择器还可以用逗号拼接为一个组合选择器,满足任意其中一个选择器的元素都会被该组合选择器选中,逗号的前后允许出现空白。某些选择器支持命名空间前缀,命名空间前缀的声明机制应该由使用选择器的语言指定,如果使用选择器的语言没有指定命名空间前缀声明机制,那么就没有前缀声明。在 CSS 中,命名空间通过 @namespace 规则声明。
(01) (02) (03)(08)
Are you sure you want to do this?
(09) Continue (10) Cancel (11)fish
(16)文档中可能包含被声明为 ID 类型的属性,ID 类型的属性的特殊之处在于一份文档中的任意两个元素不会有相同的 ID 值。无论文档语言是什么,ID 类型的属性可以用来唯一地标识它所在的元素。ID 选择器由数目符号 (U+0023, #) 和合法的 ID 值组成,ID 选择器指代具有与定位器匹配的 ID 值的文档元素,如清单 5 所示 (行末括号中的数字显示当前行定位器将定位到清单 4HTML 文档中的第几行元素)。当在 HTML 中表示 class 属性时通常将句点符号 ( U+002E) 作为~=的替代符号,因此在 HTML 中 div.value 与 div[class~=value] 具有相同的意义,均定位到 class 为 value 的 div 元素。需要特别注意的是形如 p.pastoral.marine{color:green} 的选择器将匹配到 class 属性既包含 pastoral 又包含 marine 且二者用空格分隔的 p 标签,例如匹配到 class="pastoral blue aqua marine"而匹配不到 class="pastoral blue"。类型选择器中的类型是元素类型的名称,类型选择器指代文档树中具有相同类型的元素,如清单 5 所示。通配符选择器使用星号 (* U+002A) 指代任意元素类型的合法名称。如果星号表示的通配符选择器不是简单选择器序列的唯一组件或者它的后面紧跟着一个伪元素,那么可以省略该星号而暗指含有一个通配符选择器,然而有些情况下不推荐省略,比如 div *:first-child 就比 div :first-child 更易读,不至于和 div:first-child 混淆。另外类型选择器和通配符选择器都支持命名空间前缀,命名空间前缀使用竖线 (U+007C, |) 与选择器分隔,可以为空也可以为通配符星号。在没有命名空间前缀和分隔符的情况下,若没有声明命名空间则表示任意命名空间,若已声明默认命名空间则表示默认命名空间。
属性选择器通过对元素属性进行匹配来进行选择和定位。属性选择器又分为存在和值选择器以及子串匹配属性选择器。前者又分为四种:[att] 指代具有 att 属性的元素,而不论属性的值是什么;[att=val] 指代属性的值为 val 的元素;[att~=val] 指代这样的元素,其属性值由空格分隔的多个值组成,其中一个值和 val 完全匹配;[att|=val] 指代属性值为 val 或者前缀为“val-”。子串匹配属性选择器提供了匹配属性值子串的能力,比如 [att^=val] 指代属性值前缀为 val 的元素;[att$=val] 指代属性值后缀为 val 的元素;[att*=val] 指代属性值中含有 val 字符串的元素。在这两种属性选择器中属性值必须为 CSS 合法字符,大小写敏感度与具体的文档语言有关。详细示例如清单 5 所示。属性选择器也支持命名空间前缀,与类型和通配符选择器不同的是对于没有命名空间前缀或者命名空间前缀为空的属性选择器,其只匹配到没有命名空间前缀的元素。
伪类选择器可以说是定位 Web 元素的最后救命稻草,它允许使用文档树以外的信息选取元素,这能完成很多其它选择器不能完成的工作。伪类总是由一个前面加了冒号的伪类名组成,伪类可以存在于所有的选择器序列中,它可以位于主选择器或者通配符选择器之后。伪类区分大小写,有些伪类是互斥的,有些伪类则可以同时使用。伪类可以是动态的,某种意义上说随着用户与文档的交互一个伪类可以获得或者失去对一个元素的匹配。伪类从实现方式可分为动态伪类(如互斥的:link 和:visited, :hover, :active, :focus),目的伪类(:target),语言伪类(:lang),元素状态伪类(:enabled, :disabled, :checked, :indeterminate),结构伪类,内含伪类和否定伪类,若以定位元素为目的的话这里只有结构伪类和内含伪类才对我们有用,其他伪类主要应用在元素样式渲染领域特别是随着用户操作自动改变元素样式的场景。
结构伪类可以基于文档树的结构定位元素,其中标准文本和其它非节点元素没有被算进父节点的子节点列表中,子节点的位置索引从 1 开始计算。结构伪类包含十二种::root 伪类指代文档的根节点,在 Html4 中通常指 HTML 元素;:nth-child(an+b), n>=0 伪类指代在第 an+b 个孩子节点,即其之前有 an+b-1 个兄弟节点的元素,举例来说:nth-child(2n) 指代第偶数个孩子节点,:nth-child(2n+1) 指代第奇数个孩子节点,:nth-child(4) 指代第 4 个元素孩子节点;tag:nth-of-type(an+b), n>=0 指代第 an+b 个类型为 tag 的孩子节点,非 tag 类型的节点不参与计数;:nth-last-child(an+b),n>=0 伪类指代从后往前数第 an+b 个孩子节点,即其后有 an+b-1 个兄弟节点;:nth-last-of-type 指代从后往前数第 an+b 个类型为 tag 的节点;:first-child,:last-child,:first-of-type,:last-of-type 分别是上述四种伪类的特例,指代第一个或者最后一个孩子节点;:only-child 伪类指代没有兄弟节点的孩子节点;:only-of-type 伪类指代没有相同类型的兄弟节点的孩子节点;:empty 伪类指代没有孩子节点的节点。详细示例见清单 5。
内含伪类(:contains())通过元素上包含的文本来定位元素,由于在翻译验证性测试的特殊性,该定位方法需配合英文文本与本地化翻译文本的映射才能有效使用。
//by ID selector#loginForm(03)//by Class selector.ask(08)/by Type selectorp.ask(08)//by Universal selector*|*(all)//by attribute presence and value selectorinput[value](06)input[name="continue"](06)input[class~="required"][name="password"](05)a[hreflang|="en"](10)//by substring matching attribute selectora[href^="cancel"](10)a[name$="name"](04)a[name*="pass"](05)//by structural pseudo-classesul#structure li:nth-child(4)//只有当第四个子元素是 li 时才可成功定位到 (16)ul#structure li:nth-of-type(4)(17)ul#structure nth-last-child(3)(15)ul#structure li:nth-last-of-type(3)(14)//by contains pseudo-classli:contains("Cat")(13)关系选择器
由于编码不规范等问题,仅仅通过简单选择器有时候并不能唯一定位到我们预期得到的元素,这时我们还可以通过元素间的代继关系,首先定位到目标元素的祖先或父亲或兄弟节点,然后借助关系选择器进行定位。常见的关系选择器有后代选择器、子选择器和兄弟选择器三种:后代选择器使用空格分隔两个简单选择器以代表二者具有祖先、后代关系,形如“A B”的后代选择器代表目标元素 B 为 A 的任意后代元素;子选择器使用大于号分隔两个简单选择器以代表其父子关系,形如“A B”的子选择器代表目标元素 B 为 A 的子元素;兄弟选择器分为两种,相邻兄弟选择器和一般兄弟选择器。相邻兄弟选择器使用加号分隔两个简单选择器以代表二者具有相同的父元素且位置临近,形如“A + B”的兄弟选择器代表目标元素 B 为紧随 A 后面的兄弟元素。一般兄弟选择器使用波浪号分隔两个简单选择器以代表后者与前者具有相同的父元素,而并不要求这两个元素位置临近。如清单 6 所示:
//by Descendant combinatordiv p(15)//by Child combinatorsbody > p(08)//by General sibling combinatorform + p 或者 div ~ p(08)
由上文可见,在编写翻译验证性测试自动化脚本的时候一般这样依次选取定位元素的方法:有 ID 或者唯一 name 属性就直接用其定位。对于无 ID 或唯一 name 的情况,属性组合能唯一定位的可采用 CSS 属性选择器,文档结构能唯一定位的可采用 CSS 结构伪类选择器;对于 CSS 属性选择器和结构伪类仍不能解决的情况,如果相关元素可以精确定位的话还可以借助关系选择器进行定位。最后,虽然翻译验证性测试自动化脚本“一次编写,多个语言环境执行”的特点限制了 CSS 内含伪类的开箱即用,但是如果能借助外部程序实现用户界面中需要翻译的英文字符串与翻译好的本地化字符串之间的映射的话,可以直接在英文环境下编写自动化脚本时将英文字符串作为内含伪类的参数定位元素,当在其他本地化环境下运行脚本时将内含伪类的参数替换为本地化字符串即可。
此外,有时会发现明明定位器语法正确但是仍然无法获得元素,这可能是由于 iframe 存在的缘故。对于 iframe 里的元素,要先显示进入那个 iframe 之后才可定位(比如在 selenium IDE 里要先 selectFrame 进入那个 iframe,参数为 iframe 的名字)。
最后,实际应用中一般无需完全掌握 XPath 或 CSS 选择器的语法,利用一些诸如 XPath Checker和 Firebug/FirePath 的浏览器插件可以方便地获取 XPath/CSS Path 并验证其正确性(一般在插件控制台中可以使用$$() 验证 CSS Path,利用 $x() 验证 XPath)。
综上所述,由于 Web 技术的发展及翻译验证性自动化测试需求的特殊性,在编写 Selenium 自动化脚本定位元素时存在着一些难点,本文通过实例详述了各种定位尤其是 CSS Path 定位的方法,并针对上述难点分享了一些个人见解,希望对大家有所启发。