最近在重读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,
...
...
分别用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:
- 以上定位代码均可以在https://www.whitehouse.gov/中试用
- 用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
的
标签,以做后续跳转。
想要定位到某个元素的话,首先应该考虑这个元素有没有唯一属性,例如id。
如果没有唯一属性的话,再考虑从这个元素的父元素、祖宗元素、隔壁元素中找到唯一属性,然后再定位到这个元素(参考)。
//通过有唯一属性的父元素,向下定位到目标元素。注意"空格"和">"的区别
@FindBy( css = "li[data-sku='13435315793'] .p-img>a")
这样一来,不管这个数据的位置如何改变,只要改商品的标志,即13435315793
不变,测试用例都不需要额外的维护了。
动态生成的视图
有些网站中,会存在一些动态生成的视图。它们通常随机生成标志(ID),或者是在ID/CLASS后面增加无序随机字符串作为标识;有时候甚至会在同一个页面中生成多个相同的元素ID。这就导致元素定位工作变得更加麻烦。例如下面这个网站,在一个页面上生成多个带data-view
属性的div,其value后面的三位数字为随机生成。
要定位到页面上“第二大类问题的第二个小问题”里面的这个input框,使用工具复制下该元素的XPath或者CSS选择器的时候,发现复制下来的定位器查找到的元素不止一个。原因就是视图动态生成,生成了很多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-inline
class的元素(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选择器以及其他定位器可以让我们事半功倍。定位元素可遵循如下思路:
- 找到待定位元素的唯一属性。
- 如果该元素没有唯一属性,则先找到能被唯一定位到的父元素/子元素/相邻元素,再使用
>
, 空格,以及+
等进行辅助定位。
- 不要使用随机唯一属性定位。
- 最重要的是多跟研发沟通,尽量把关键元素加上ID或者name,并减少不合理的页面元素,例如重复ID这样的事情最好不要发生。