查看原文
其他

震惊!!Xpath封装还能这么玩?

胡迪 酷家乐技术质量 2023-02-22

背景

酷家乐有一套自己的UI自动化框架--Hades,其主要以puppeteer与playwright为核心进行了二次封装改造,并整合了许多酷家乐设计工具前端api。使得UI自动化对canvas交互、前端性能测试有比较好的支持。

除了能力上的扩展以外,Hades还有一个显著特点是:它将puppeteer/playwright中的api都代理到一个pyBell对象上,使得我们无需关注browser、page、element等对象,大大简化了用例编写的难度。


对于酷家乐设计工具而言,UI 自动化是比较重要的一个上线回归手段,目前整体自动化用例数已经超过1500条,参与编写自动化case的同学众多。

在这样的背景下,我们对自动化case编写门槛、编写效率,以及稳定性有比较高的要求。为此,我们探索与积累了许多技巧,包括 自动化用例编写规范、xpath 封装技巧、前端测试辅助工具开发、VScode 辅助插件开发 等。


本篇将分享几例 xpath 元素定位封装的技巧。

一、从一个用例的演变开始

由于酷家乐设计工具为大型单体应用,不存在page的概念,并且许多”按钮“、”菜单“元素都是动态加载,再加上框架封装的特点,所以业界通用的PageObject设计模式并不完全适用。

我们采用的方式是将用例分为三层:

  • selector:定义一些 xpath、css selector。

  • function:封装一些较为通用的逻辑函数。通常以npm二方包形式存在,便于跨业务线共享(也有部分特定的函数封装封装在用例repo中,未提供二方包)。

  • testcase:编写的test case。通过调用一些公共函数+直接操作元素组成我们的业务用例。


在早期,我们的用例往往是这种风格。

其特点是:

  • 用例内容较直观,但是业务函数封装过少,可读性较差。用例代码量大。

  • xpath定义冗余,可维护性不理想。


UI自动化始终不能脱离的一个痛点是xpath(或 css selector),很多新人往往会在调试 xpath 元素定位上花费大量时间,并且最终写出一些不易读、不易维护的元素定位。

为此,我们探索了一些函数封装、xpath封装的技巧,加强了一些封装与用例规范。现阶段的用例演变为这种风格。

其特点是:

  • 因为加强了业务函数封装,用例中几乎完全隔离了 xpath,可读性与可维护性大大提升。

  • xpath 也进行了函数化,编写的 xpath 比之前减少70%以上。xpath 的稳定性也提升了不少。

  • 用例代码量大大降低,对新人比较友好。

二、函数封装技巧

2.1 xpath函数封装,减少70%的xpath编写

函数封装并不局限于元素操作。其实将函数运用到 xpat h编写,也能带来巨大的价值。

以酷家乐工具顶部栏举例,如果想点击 撤销、保存、hover 文件,可以有以下三种定义xpath元素方法:

方式一:偷懒式,使用 pyBell.clickByText()函数

// 无需定义xpath。该函数本质上等于 await pyBell.click("//*[text()='撤销'"] await pyBell.clickByText("撤销")await pyBell.hoverByText("文件")

缺点:该函数本质是用了"//*[text()='xxx']"这样的xpath定位,所以很容易找到重复的元素,导致操作与预期不符。尤其是酷家乐设计工具为大型单体应用,定位到多个相同文本元素的概率特别大。优点:使用简单。无需自己定义xpath。


方式二:传统式,为每个元素定义一个xpath

// 每个元素单独定义xpathconst undoButton = "//*[contains(@class,'TopBar-root')]//*[text()='撤销']"const saveButton = "//*[contains(@class,'TopBar-root')]//*[text()='保存']"const fileButton = "//*[contains(@class,'TopBar-root')]//*[text()='文件']"
await pyBell.click(undoButton)await pyBell.click(saveButton)await pyBell.hover(fileButton)

缺点:每个按钮都要定义一个xpath。重复内容多,维护成本高,可读性差。优点:非常精确。


方式三:xpath函数化

// 定义一个xpath,利用按钮名称的差异,支持所有按钮元素const TopBarMenu = (name) => `//*[contains(@class,'TopBar-root')]//*[text()='${name}']`
await pyBell.click(TopBarMenu("撤销"))await pyBell.click(TopBarMenu("保存"))await pyBell.hover(TopBarMenu("文件"))

缺点:对多语言不友好(可忽略)。优点:非常精确。可读性高,使用简单。一个xpath覆盖所有按钮。大大降低了定义xpath的工作量。


有了xpath函数,还可以进一步将其与业务函数结合,让用例与xpath完全解耦。

// 定义xpath函数const TopBarMenu = (name) => `//*[contains(@class,'TopBar-root')]//*[text()='${name}']`
// 定义顶部栏click函数async function TopBarClick(menuName) { // 可以加入一些其它逻辑 await pyBell.waitFor(TopBarMenu(menuName)) await pyBell.click(TopBarMenu(menuName))}// 定义顶部栏hover函数async function TopBarHover(menuName) { // 可以加入一些其它逻辑 await pyBell.waitFor(TopBarMenu(menuName)) await pyBell.hover(TopBarMenu(menuName))}
// 函数使用await TopBarClick("撤销")await TopBarClick("保存")await TopBarHover("文件")await TopBarHover("渲染") // 支持顶部栏所有按钮。完全与xpath解耦。

2.2 DOM元素相对位置+xpath函数通过上面的技巧,就实现了通过 一个 xpath + 两个函数 覆盖了20+的按钮操作,大大降低了xpath编写量,并且提升了可读性、可维护性。

前面提到的场景中,元素都有中文名称,对于没有名称的input,也可以根据DOM元素相对位置找到关联关系。

举例:酷家乐设计工具中参数面板使用的频率较高,但是不同模型的参数名称都不一样,参数数量也不一样。如果对每个参数定义一个xpath,简直可怕。

思路:通过分析参数面板Dom结构特点,可以发现参数名元素与input元素之间的相对位置关系是确定的。所以可以想办法通过参数名定位到input元素。

以下有两种xpath写法:

// 方法一:根据子节点+层级关系 查找祖先节点// 先找到参数名,然后找到参数名和input的共有祖先节点,然后定位到inputlet input = (text)=>`//div[contains(@class,'FunctionPanel-root')]//*[text()='${text}']/../../..//input`
// 方法二:根据子节点+祖先节点关键字 查找期望的祖先节点// 不用..的方式找祖先节点,通过ancestor关键字查找所有祖先节点,然后找到含关键字的最近的父类let input = (text)=>`//div[contains(@class,'FunctionPanel-root')]//*[text()='${text}']/ancestor::div[contains(@class,'root')][1]//input`
// 函数使用。设置宽深高await pyBell.keyboardType(input("宽度"), 200)await pyBell.keyboardType(input("深度"), 100)
//再结合前面提到的函数技巧,就可以很轻松封装成下面这种函数:await setParams({"宽度":"200", "深度":"100"})

2.3 定义xpath中文key,提升可读性通过以上的技巧,我们仅仅用了 1个xpath+1个函数 就搞定了参数面板所有input元素,无论该模型自定义了任何参数都能支持,大大降低了后续的维护工作量。

除了input以外,还有一类元素即没有名称也没有关联的名称。我们可以直接定义其通用的中文名。

举例:以酷家乐设计工具中的selectMenu举例,selectMenu存在许多按钮,每个按钮存在名称,但是xpath无法通过text定位。

// 方法一:通常我们会用英文key定义xpath,可读性比较差const selectMenus = {    // 复制    copy: "//div[starts-with(@class, 'SelectMenu-content-wrapper_')]//i[@class='tui-icon tui-icon-select-menu-copy']",    // 删除    delete: "//div[starts-with(@class, 'SelectMenu-content-wrapper_')]//i[starts-with(@class, 'tui-icon tui-icon-select-menu-delete')]",}// 点击删除await pyBell.click(selectMenus.delete)// 点击复制await pyBell.click(selectMenus.copy)  // 方法二:改用中文key定义const selectMenus = {    "复制": "//div[starts-with(@class, 'SelectMenu-content-wrapper_')]//i[@class='tui-icon tui-icon-select-menu-copy']",    "删除": "//div[starts-with(@class, 'SelectMenu-content-wrapper_')]//i[starts-with(@class, 'tui-icon tui-icon-select-menu-delete')]"}// 定义一个selectmenu操作函数async function selectMenuClick(menuName) {    // 内部可以加一些逻辑判断,提升稳定性    await pyBell.waitFor(selectMenus[menuName])    await pyBell.click(selectMenus[menuName])} // 用例中可实现与xpath解耦。如果有新增按钮,直接增加xpath定义即可,无需维护函数await selectMenuClick("复制")await selectMenuClick("删除")

2.4 顶层元素+关键字 定义通用xpath通过这个技巧,虽然并没有减少 xpath 编写量,但是提升了可读性,并且用例中可以实现与 xpath 完全解耦。

举例:酷家乐设计工具中有很多类似这种动态生成的按钮,我们采用的xpath方式是:顶层元素+文本名称 的方式。

// 通常可以找到这些元素最顶层的元素const LeftPanel = "//*[starts-with(@class,'LeftSidePanel-root')]"// 然后在该元素内部查找文本关键字。为了处理按钮名称重复的场景,增加一个index参数const item = (name, index=1)=> `(${LeftPanel}//*[text()='${name}'])[index]`
// 简单的函数封装async function ClickItem(name, index=1){ await pyBell.click(item(name,index))}
  // 函数使用await ClickItem("组件库")await ClickItem("查看全部")await ClickItem("查看全部", 2) // 表示点击左侧面板中第2个 查看全部 按钮


2.5 合理的函数模块划分通过以上技巧,就实现用 1个函数+1个xpath 覆盖某个区域内所有按钮点击操作,并且不担心文本重复的问题。

函数封装一定是提升用例编写效率与质量的必备技巧。但是,当函数逐步增长,存在上百个函数时,一定会给“找寻合适的函数”带来很大的麻烦,反而会降低函编写效率。

所以,在封装函数前一定要做好合理的模块定义。


以酷家乐设计工具主场景为例,由于并没有page概念,所以我们将函数按照功能分为了多个模块:

  TopBar:顶部栏相关函数。

  LeftPanel:左侧面板相关函数。比如切换当前环境、搜索、拖出模型等。

  RightPanel:右侧面板相关函数,比如切换房间,修改参数、获取参数。

  SelectMenu:封装模型菜单操作。

  ResourceManager:封装资源管理器操作。


我们针对每个公共模块都定义了几个基本的点击、文本获取、文本输入等通用函数,使用者只需传入正确的文本参数即可,无需关注元素 xpath。

使用举例:

三、总结

本次共分享了五个 xpath封装的技巧,虽然每个看起来都比较简单,但正是这些小技巧的合理运用帮助我们减少了大量 xpath 代码、提升了用例可读性与编写效率,并降低了维护成本。

以酷家乐定制业务线举例,由于一些公共模块的通用 xpath函数、业务函数较为完善,即使刚接触UI自动化几天的同学,也能快速上手编写 case。对新人而言,几乎无需关注元素定位,只需要寻找合适的函数进行调用即可,可读性也比较友好。如果有遇到需要维护元素定位、覆盖新功能元素,则让经验较为丰富的同学负责处理。


以上是本篇全部内容,如果你有任何想法或建议,欢迎在我们公众号聊天对话框中发送消息。


推荐阅读



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存