• 常见的 PO 并没有变,组件化只是把元素定位和操作这块单独提出来做了抽象封装,以此提高组件的复用性,易用性,然后在 PO 里面就不再直接操作标签,而是通过组件进行交互。

    我之前在飞哥的帖子下面也有做新的回复,写了一些我做的案例,可以看一下。UI 自动化的分层设计,然后 renorex 的组件化思路是对的,从人的角度来讲,我们看到的确实是一个个组件,从开发角度讲,现在前端和客户端也都是组件式开发,selenium 早期的 locator 已经不适用了,组件化定位和操作以后肯定会像 PO 一样成为 UI 自动化规范。

    by the way,我已经转游戏客户端开发了,以后在测试方向的研究会少很多,现在也还只是出于兴趣在了解和探讨,不过技术和开发思想都是相通的,以后有想到什么好的方法和方案有利于测试的,到时候也会到 testerhome 来聊聊。

  • 看了下官网介绍,这个工具在 web 测试方面底层驱动应该也是 selenium,定位的话好像自己整合了一个 RanoreXPath 来适配多端。除此之外有整合组件化的概念,这也是我第一次专门看到一个工具在 UI 方面做组件化,和我做组件化的思路是完全一样的。然后有自己定义 action 来整合一些常用的方法,最后做了录制。虽说录制是快速做到 60,70 分,但底层做的工作还是让这个工具拥有定制化然后达到 80,90 分的能力。

    仔细看了下这个工具支持的概念和功能,还是不错的,我一个测开做的话可能至少要差不多半年时间才能做出类似的东西还不一定更好用。如果是到一个新团队然后要推 UI 自动化,我可能会更多调研之后尝试推这个工具。

  • 使用层级定位和一些 xpath 的写法,如果楼主能看明白的话,应该是可以解决定位不了的问题。

    def _get_options(self, layer: int) -> list[WebElement]:
        # 普通下拉
        xpath = f"//div[contains(@class,'el-popper') and not(contains(@style,'none'))]/descendant::ul[{layer + 1}]"
        # 添加组合商品等待下拉的按钮,会使用下面的样式
        xpath += "|//ul[contains(@class,'el-popper') and not(contains(@style,'none'))]"
        element = self._driver.find_element(By.XPATH, xpath)
        return element.find_elements(By.XPATH, ".//li")
    
  • 用 selenium 的话,应该是 web 自动化吧,selenium 是最优解,所有网页基本都是可以定位和操作的,如果做不到,说明自己理解还不够深。没事可以多看看 xpath 和 selenium 官方文档。

    另外前端组件化之后,大多组件确实只能靠 text 定位,所以我现在大多都是用 text 定位,还可以使用层级关系,先找到一个元素,然后通过元素找下级元素,和轴定位还不太一样。

    这里贴一个我写的 xpath:

    class Button(ClickMixin):
    
        def __init__(self, text: str):
            # 把当前节点的文本去除前后空格后,替换中间空格,然后比较文本
            # template = ".//{0}[translate(normalize-space(),' ','')='{1}']"
            # 暂时只开放button和a标签,避免匹配太多标签导致混乱
            a = f".//a[translate(normalize-space(),' ','')='{text}']"
            button = f".//button[translate(normalize-space(),' ','')='{text}']"
            # 支持带cursor css类的span标签
            span = f".//span[translate(normalize-space(),' ','')='{text}']"
            # vue表格操作栏的按钮
            table_btn = f".//div[.='{text}' and @class='table-handleBtn']"
            xpath = f"{a}|{button}|{span}|{table_btn}"
            super().__init__(xpath=xpath)
    
    1. 说是像培训出来的,主要是工作经历有点偏门,农业管理平台,音乐下载器等等,一看就是小公司,产品线比较混乱,这样的团队一般对测试流程也不太重视。

    2. 我说的专业理论和深度思考,其实是要求个人要有对行业和技能的理解,归纳能力强。
      例如写工作流程这段,需求评审,用例评审,提交 bug,bug 验收等等,我只能看出来候选人知道这些流程,但并不一定知道为什么要有这些流程和怎么去改进流程,把每个环节做得更好。

    3. 另外随便写点,比如学习平台项目经历这段,我可能会这样写:

    恒智 AI 学习平台
    平台分三种角色,管理员、讲师、学员,测试内容涉及管理员侧的账号管理、课程管理、学校信息管理,讲师课程管理,发布学习任务,实时讲课,课后问答等功能,学生端课程学习、在线编程,查看学习进度,报告等功能。

    项目期间能够从多角度,用户需求出发,主动及时发现问题,提升体验,保障产品质量,遇到问题能够认真分析问题,和开发高效沟通并协力解决问题,提高产品交付质量和效率。

  • 简历给人的感觉像是培训出来伪造的经历,工作经历偏门,业务梳理不清晰,技能太浅显表面,感觉缺乏专业的理论和深度思考。

  • UI 自动化中的分层设计 at 2022年08月24日

    时隔大半年,自己在这中间也在公司实践 UI 自动化,结合自己的经验和网上的案例与理念,造了一些轮子。

    一、组件化:

    class Page(metaclass=ABCMeta):
    
        def __init__(self, driver: Chrome):
            self._driver = driver
            # 每一个页面都有独立的组件,所以page方法不能覆盖,需要全部调用一遍,这里是遍历继承链,把所有父类和本身的page方法都执行一遍
            for base in self.__class__.__mro__[::-1]:
                if issubclass(base, Page):
                    base.__page__(self)
    
        @abstractmethod
        def __page__(self):
            pass
    
        # 使用self.btn_submit = Element()时调用,注入driver到component
        def __setattr__(self, name, component):
            if issubclass(component.__class__, Component):
                component.set(driver=self._driver)
            super().__setattr__(name, component)
    
    
    class LoginPage(Page):
        def __page__(self):
            self._input_username = Input("请输入用户名")
            self._input_password = Input("请输入登录密码")
            self._btn_login = Button("登录")
    
        def login(self, username: str = "admin", password: str = "admin"):
            self._input_username.send_keys(username)
            self._input_password.send_keys(password)
            self._btn_login.click()
    
    
    # 使用方式
    # LoginPage(driver).login()
    

    实际项目中封装的组件很多,除输入框、按钮外、还有日期选择,日期范围选择、表格、下拉等,适配 element、antd 等各个前端框架。组件化的核心理念在于类内部存储定位方式,使用惰性加载的方式后置传 driver,执行操作时动态查找元素,还用到了 xpath 的或条件等增加组件的兼容性。

    二、链式调用:
    这个其实使用范围很广了,但我看很多人写 UI 自动化还是没按这个理念去设计,所以还是贴一下,可以简化代码和增加可读性。

    # 链式调用示例
    page = ProductIndexPage(driver)
    (
        page.navigate_to(PriceCalculatorPage)
            .add_product_info("电脑整机", "笔记本", "戴尔", "E7240")
            .choose_product_level("A")
            .choose_product_config({"CPU": "i7 8代", "内存": "4G"})
            .choose_product_property()
            .choose_other_property(is_new=True, is_return_after_rental=True, use_channel=0)
            .fill_in_price_valid_date(5000, 5000, False, "厂商", (3, 7, "天"))
            .fill_in_residual_value(5000)
            .compute()
            .save()
    )
    
    # 方法内部实现
    class PriceCalculatorPage(ProductIndexPage):
        def choose_product_level(self, level: str):
            """选择商品等级
    
            Args:
                level: A、B、C
            """
            self._select_product_level.select_by_text(level)
            return self
        def compute(self):
            """点击计算"""
            self._btn_compute.click(delay=1)
            self.scroll_to_end()
            return self
    
  • func1 最后一行是 return func2,func2 不加括号就是返回 func2 这个函数本体,所以执行 func3 = func1(1) 的时候相当于 func3 = func2,再执行 func3(2) 时等同于执行 func2(2)

    python 里函数也是对象,可以用来作为参数传递和赋值,这里贴一个高阶函数的教程https://www.liaoxuefeng.com/wiki/1016959663602400/1017328525009056

  • 之前也见过一些人在做录制回放和云真机平台的集成,确实降低了使用要求和学习成本,作者能够完成这样一个平台并落地,发文章出来,想来也应该是有一定成效,但还是有一些疑问希望提出来大家共同探讨。

    1. UI 通常包含多个部分的内容,例如样式、文案、动态内容和用户状态,使用截图对比的方式会不会 UI 稍微一变动就会报错,缺少容错性;截图对比具体是靠什么指标去判断断言是否通过呢,相似度么,会有一个绝对差值么。

    2. 如果一条用例中有一小部分改动了,这个时候怎样去维护用例呢,修改 solopi 指令不能达到模拟手动操作的效果会需要重新录制脚本么。

    3. 用截图对比和录制的方式,可以做到兼容性测试么,录制的用例能否覆盖多个主流机型。

    4. 开发这样一个 UI 自动化平台大概需要多少人天

  • 同楼上,楼主在避免处理魔术方法时把带__的方法都漏掉了,漏掉了__classcell__,所以报 DeprecationWarning,高版本 python 应该会报 RuntimeError,漏掉了__init__,类创建后默认使用了无参初始化方法,所以报 MetaClassTest() takes no arguments

    class LowerAttrMetaClass_4(type):
        def __new__(mcs, name, bases, attr):
            attr = {key if key[:2] == "__" else key.lower(): value for key, value in attr.items()}
            return type.__new__(mcs, name, bases, attr)
    
    
    class MetaClassTest(metaclass=LowerAttrMetaClass_4):
        Say = "hello world"
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def Run(self):
            print("MetaClassTest run method")
    
    
    tmp_obj = MetaClassTest(name="zzz", age=20)
    tmp_obj.run()
    # >>>
    # MetaClassTest run method