Selenium [Selenium CSS locator] 记一次元素定位优化行动

大大灰灰狼 · 2017年09月25日 · 最后由 Karaser 回复于 2017年10月19日 · 2896 次阅读
本帖已被设为精华帖!

最近在重读 Web 测试囧事的自动化这章。在读到书中总结的 UI 自动化的经验的时候,脑袋里面闪现出了自己项目中的一些 Web UI 自动化代码,突然觉得这些代码的可读性和可维护性都可以再提升一下。

目前项目中有一些痛点,例如页面显示的是一个商品 List,那么添加、删除一些测试数据的时候,有可能会改变现有数据的位置。这就要求在维护测试数据时,需要着重考虑是否会影响已有的自动化测试结果。再例如有些元素为动态生成,或者随机数作为 ID 属性,这样的元素定位通常会比较困难。在项目的日常工作中,会花费大量的时间在维护这些测试脚本上面。

先尝试从最简单和代价最小的方式入手进行改造,以提升测试稳定性和客户维护性。想了一下上面列举的两大痛点,应该可以通过改造元素定位方式来解决。

定位器使用优先级

我们常用的定位器通常有:

  • XPath
  • CSS selector
  • ID
  • Name
  • 其他

针对这些定位器的使用优先级,书中推荐的优先级是

ID>Name>CSS>XPath

原因是 ID 和 Name 最直观和最易维护的,而且不受 HTML 文档结构影响,即无论元素变化到哪个位置都可以被定位到。而 ID 和 Name 相比的话,具有不可重复性这样的特点,所以优先级更高 (有些网站会出现重复的 ID 甚至 XPath,这样的 Web 是不符合可及性及 Web 开发规范的。这样的网站存在,但是不合理)。

XPath 和 CSS 选择器比较

这两者之间的比较自古就存在,围观群众对孰优孰劣的问题各持己见。我的看法是:

从工作的机制来看,XPath 使用路径标记在 XML 文档层次结构中进行导航,简单说就是遍历文档路径。Selector则是一种匹配模式,并且针对 XML、HTML 做了优化,速度上优于 XPath。除此之外,网站上的不同元素 CSS,也是通过 selector 去定位和绑定的。

从书写形式上看,CSS 选择器提供简单的语法,稍加学习即可掌握。举个例子,假设我们有这么一段 HTML,

<div class="field-item odd">
  <div>...<div>
</div>
<div class="field-item even">
  <div>...<div>
</div>

分别用 XPath 和 CSS 选择器定位class="field-item odd"的 div,效果如下:

//XPath
@FindBy( xpath = "//*[contains(concat(\' \', normalize-space(@class), \' \'), \'field-item odd\')]")
//XPath
@FindBy( xpath = "//*[@class='field-item odd']")
//CSS selector
@FindBy( css = ".field-item.odd")

Tips:

  1. 以上定位代码均可以在https://www.whitehouse.gov/中试用
  2. 用 Firefox 或者 Chrome inspector 查看元素,可以直接使用 XPath 或者 CSS 选择器搜索页面元素

所以我觉得 CSS 选择器在书写的方便和阅读的直观程度上,也略占优势。

但是 XPath 也有它独特的优势,比如 “查找包含某个子元素的父元素” 这样的操作,CSS 选择器就暂时无法做到 (不过实战中也极少用到)。

最重要的一点,在我采访了多位专注前端开发数十年的研发后发现,前端开发们纷纷表示对 CSS 选择器熟悉,并且平时的工作中也会经常使用;而对 XPath 则没那么多的了解 (因为几乎用不到)。考虑到今后自动化的脚本开发们也可以帮忙维护,再加上新入坑的同学可以由开发来做一些指导,所以 CSS 选择器再得一分。

综上,我决定对项目中的定位方式改造和维护时,以 CSS 选择器为主,实在不行再使用 XPath 补充。

解决问题

变化的 ListItem 位置

在项目老代码里,传统定位方式是从 Chrome 中直接复制下目标元素的 XPath 进行定位。所以经常会在代码里面看到很长很长的一串的定位字符串,例如

//定位白宫官网FEATURED CONTENT的第一个图片
image1 = "//*[@id="page"]/div[2]/div/div/div[2]/div/div/div/div/div[1]/div/div[1]/a/div/div/div/img"

这样的定位代码既难度,又容易失效。

举个例子,假设现在有一个自动化的用例是:打开 JD 商城,搜索 “web 测试囧事”,然后找到 “旷氏文豪图书专营店” 的这条记录,点进去最后验证书店名称和价格。
京东商城截图
那么按照传统做法,复制下这条记录的 XPath,以定位这个商品,再做后续操作:

//*[@id="J_goodsList"]/ul/li[4]/div/div[1]/a

我们来解读下这个 XPath:解释器会先找到 id 为J_goodsList这个元素,然后找到这个元素下的第一个ul标签,再去找到ui下面的第 4 个li,以此类推,最后找到目标a标签。这样根据 XML(html) 文档结构的绝对位置产生的定位方式,我喜欢叫它绝对路径定位。

那么问题来了,假设产品给了个新需求,要求商品默认按照价格降序排序;或者是测试场景变更为:先删除第三个商家的产品,然后再定位到 “旷氏..” 这条记录。这时候页面的文档结构必然发生改变的时候 (商品位置变更),之前定位的绝对位置指向的元素就发生了改变,那不得又得重新维护一次脚本了?

所以是时候重新考虑一下定位方式了。观察后发现,数据变化后仅会引起商品绝对位置的改变,对商品内部的 icon,文字描述等则没有影响。那么,使用相对模糊的定位方式,应该可以解决这个问题。根据之前定位器优先级的调研结果,优先考虑使用 CSS 选择器重写。

还是以上面的商品为例子,在商品列表页面用 CSS 选择器定位到这个带herf<a>标签,以做后续跳转。

html文档结构

想要定位到某个元素的话,首先应该考虑这个元素有没有唯一属性,例如 id。
如果没有唯一属性的话,再考虑从这个元素的父元素、祖宗元素、隔壁元素中找到唯一属性,然后再定位到这个元素 (参考)。

//通过有唯一属性的父元素,向下定位到目标元素。注意"空格"和">"的区别
@FindBy( css = "li[data-sku='13435315793'] .p-img>a")

这样一来,不管这个数据的位置如何改变,只要改商品的标志,即13435315793不变,测试用例都不需要额外的维护了。

动态生成的视图

有些网站中,会存在一些动态生成的视图。它们通常随机生成标志 (ID),或者是在 ID/CLASS 后面增加无序随机字符串作为标识;有时候甚至会在同一个页面中生成多个相同的元素 ID。这就导致元素定位工作变得更加麻烦。例如下面这个网站,在一个页面上生成多个带data-view属性的 div,其 value 后面的三位数字为随机生成。

某运动类网站

要定位到页面上 “第二大类问题的第二个小问题” 里面的这个 input 框,使用工具复制下该元素的 XPath 或者 CSS 选择器的时候,发现复制下来的定位器查找到的元素不止一个。原因就是视图动态生成,生成了很多 ID 重复的元素,而通过工具复制下来的定位器会优先选择 ID 属性。

ID相同的元素

动态视图大多都通过模板生成,所以这类视图的内部结构通常都是一毛一样的。所以只要能定位到这个视图,后续再在这个视图中根据文档结构找到这个元素就可以了。

上面例子中的视图 div 的唯一标志 data-veiw 的值是随机的,每次打开页面都会更新,但是每个视图在页面上的顺序不会发生改变。所以这时候我选择从更上级的元素开始定位,通过nth-child来确定查找第几个视图中的元素。

@FindBy( css = ".multiRegistration__category-list>div:nth-child(2) .form-inline>div:nth-child(2) input")

先定位到更上级包含了所有问题的 div,其 class 为.multiRegistration__category-list,然后向下一级查找到第二个 div,即 “第二个大类的问题”;再查找这个元素的所有下级元素中查找包含.form-inlineclass 的元素 (class 全写为select-race__registrationCategory form-inline),然后向下一级查找到第二个 div,即 “第二个小问题”;最后再在其所有子元素下面找到 input 标签。需要注意的是每一个步骤查找出来的元素都应该是唯一的。

这样的定位方式我觉得算是绝对路径定位方式的升级版,绝对路径定位和模糊定位相结合。相比直接按照文档接口定位的方式来看,少了很多层级关系和减轻了书写的难度;从维护角度考虑,尽量减少>符号这种查找直属下级的定位方可以有效提高容错率,模板结构不做巨大变更的话,无需维护脚本。

额外的疗效

学习完了 CSS 选择器后,发现这些定位元素的方法也可以拿到浏览器中去直接执行。再结合一些简单的 JavaScript 脚本,甚至可以实现页面半自动测试。对需要大量重复手动作业、设置代码运行环境困难、觉得各种浏览器 driver 和 selenium 版本不好对应、懒癌晚期的同学来说简直是天降福音。

这种方法可以在各种浏览器自带的开发工具 (windows 下通常可以通过 F12 调出) 里的 console 里面直接执行。随便找个浏览器打开京东首页,然后在 console 中粘贴下面这段代码,敲下回车试试看会发生什么:

javascript:(function(){$("#key").focus().val("web测试");$(".form button").click()})()

对于没有使用 jQuery 的网站,也可以使用 JavaScript 原始方法定位,例如document.querySelector('#id')
除此之外,还也可以把代码放到书签里面,通过点击直接执行。效果如下:

点击标签自动执行填空

总结

在面对使用不同技术栈、不同风格、不同编码水平的前端页面的时候,对我们定位元素会有一些挑战。灵活的运用 CSS 选择器以及其他定位器可以让我们事半功倍。定位元素可遵循如下思路:

  1. 找到待定位元素的唯一属性。
  2. 如果该元素没有唯一属性,则先找到能被唯一定位到的父元素/子元素/相邻元素,再使用>, 空格,以及+等进行辅助定位。
  3. 不要使用随机唯一属性定位。
  4. 最重要的是多跟研发沟通,尽量把关键元素加上 ID 或者 name,并减少不合理的页面元素,例如重复 ID 这样的事情最好不要发生。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 8 条回复 时间 点赞

额,个人觉得用 CSS 和 XPATH 其实没有太大的区别,如果你要说 XPATH 效率慢的话,那自动化本身就不是一个讲效率的东西,也没必要为那一两秒的时间去计较。
在 Webdriver 的源代码里面,即使是通过 By.id 去定位某个元素,在源代码里面也会有一个补充的地方,如果通过 By.id 没有 get 到元素,那么也会用一次 XPATH 再查一次。
至于 JD 商城的那个 case,其实用 CSS 和 XPATH 本质都是一样的,都是通过元素属性去定位的,CSS 不太懂,但我更多时候用 xpath 是为了顺便检查一下页面有没有变形之类的,通过这种方法来间接使脚本也做了一次 UI 的检查。

但是用 CSS 和 XPATH,这两个东西并没有错,看个人喜好就行。

一直习惯了用 xpath

css 建议注入 sizzle

┑( ̄Д  ̄)┍
无奈的耸耸肩

大家在开始玩儿 UI 自动化的时候,几乎都是从 xpath 开始的, 我也不知道为啥 ┑( ̄Д  ̄)┍
然后在某次跑一大堆脚本但是时候发现了比较严重的性能问题,分析问题原因然后看到了这个,才开始动起了换 CSS 的脑筋。然后发现 CSS selector 越用越顺手..

思寒_seveniruby 将本帖设为了精华贴 09月25日 19:51
codyL 回复

很对

自己优化一下 Xpath,不要直接元素右键,这样会快很多。

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

谢谢分享,最近的确体会到 xpath 有点慢,但是在原生 app 下元素并不唯一,appium 下,不用 xpath 能有什么好用的?😨

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册