一、测试团队越做越累,不是人不够,是技能太散
上个月,一个中型电商团队找到我做技术评审。他们有三个测试小组:Web、App、接口。Web组用Playwright,App组用Appium,接口组用Requests + Pytest。三个组,三套代码仓库,三种定位器写法,三种等待策略。新人进来,要先学三套东西。一个跨端场景——比如从Web下单,App确认收货——要三个组各写一遍,再用消息队列串起来。
他们来问:要不要裁掉一组人,或者统一用某个商业平台?
答案很简单:问题不在人,在你们的技能没有统一抽象。
每一端都在做类似的事:点击、输入、获取文本、等待条件、发送请求、断言响应。但每个框架都用自己的方式表达这些“技能”。Web的“点击”是page.click(locator),App的“点击”是element.click(),接口的“请求”是requests.post(url, data)。本质上,它们都是“执行一个动作并验证结果”。但你们的代码里,每一层都在重复实现调度、重试、日志、断言。这不是技术债,这是架构债。
花了三周时间,把过去几年在多个项目里积累的经验抽出来,做了一个通用测试Skills框架。核心目标很简单:一套技能描述,同时驱动Web、App、接口。统一调用方式,统一技能注册,统一结果断言。代码已经开源。下面讲清楚它怎么工作。
二、本质不是缺框架,是缺“统一调用层”
很多人一听到“统一框架”,第一反应是再做一套超级框架,把所有底层都包进去。那是错误的思路。正确的思路是:不要在底层统一,要在“技能调用层”统一。
什么是技能?技能是一个可命名的、有输入输出、有执行逻辑的最小测试单元。比如:
click(selector)是一个技能input_text(selector, text)是一个技能http_get(url)是一个技能wait_for_element(selector, timeout)是一个技能assert_text_contains(text)是一个技能
Web端需要这些技能,App端也需要,接口端需要的只是其中一部分。关键在于:技能的调用方不关心技能背后是Playwright、Appium还是Requests。它只关心技能的名字和参数。这就好比你在写业务代码时调用一个函数,你不管这个函数是用Go写的还是Python写的。所以我们需要的不是统一的执行引擎,而是统一的技能注册表 + 动态调度器。这个框架干的就是这件事。
三、核心机制拆解:Skill抽象 + 注册中心 + 动态调度
先看架构图。

拆解三个核心机制。
机制一:Skill定义规范 - 让每个技能自描述
一个Skill的最小定义:
@register_skill("click")
def skill_click(selector: str, timeout: int = 5, **context):
"""点击指定元素,支持Web/App统一selector"""
driver = context["driver"] # 由调度器注入
# driver可能是Playwright的Page,也可能是Appium的WebElement
driver.click(selector, timeout=timeout)但这样还不够。每个底层驱动的API不同。所以真正的Skill实现是一个适配器:
class ClickSkill(BaseSkill):
name = "click"
parameters = {"selector": str, "timeout": int}
def execute(self, params, context):
driver = context["driver"]
if driver.__class__.__name__.startswith("Playwright"):
driver.locator(params["selector"]).click(timeout=params["timeout"])
elif driver.__class__.__name__.startswith("Appium"):
driver.find_element(AppiumBy.XPATH, params["selector"]).click()
# 接口层不支持click,调用会报错关键点:技能内部知道当前driver是什么类型,自己做适配。调用方完全不用管。
机制二:注册中心 - 技能的市场
所有技能启动时注册到中心。注册表维护一个字典:skill_name -> SkillClass。调度器收到调用请求后,去注册表找技能,实例化,调用execute。好处:新增技能不需要修改调度器代码。团队可以共享技能库,比如login_with_retry、wait_for_toast。
机制三:动态调度 - 一套DSL跑通所有端
调度器接受两种输入:
- YAML/JSON序列:适合关键字驱动
- Python链式调用:适合代码风格
一个YAML用例示例:
name: 跨端下单流程
skills:
- name: na vigate
params: {url: "https://xxx.com"}
- name: click
params: {selector: "#add-to-cart"}
- name: wait_for_element
params: {selector: ".cart-badge", timeout: 5}
- name: http_post
params: {url: "/api/checkout", data: {"item_id": 123}}
- name: assert_status_code
params: {expected: 200}这个用例可以在Web环境跑(na vigate, click),也可以在纯接口环境跑(http_post, assert_status_code)。调度器会根据当前注册的技能集合,跳过不可用的技能(如click在接口环境自动跳过并报警)。
核心设计哲学:技能是原子能力,用例是技能的有序组合。底层驱动可以换,技能可以增删,但用例结构不变。
四、典型案例对比:同一个场景,三种终端,一套写法
拿“登录并校验”这个场景举例。
传统方式:三套代码
Web端:
page.goto("/login")
page.fill("#username", "test")
page.fill("#password", "pass")
page.click("button")
page.wait_for_selector(".welcome")
assert page.text_content(".welcome") == "欢迎"App端(类似,但API不同):
driver.find_element(By.ID, "username").send_keys("test")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.ID, "login_btn").click()
wait = WebDriverWait(driver, 5)
wait.until(EC.visibility_of_element_located((By.CLASS_NAME, "welcome")))
assert driver.find_element(By.CLASS_NAME, "welcome").text == "欢迎"接口端:
resp = requests.post("/login", json={"user": "test", "pwd": "pass"})
assert resp.status_code == 200
assert "欢迎" in resp.textSkills框架方式:一套用例
skills:
- name: na vigate
params: {url: "/login"}
- name: input_text
params: {selector: "#username", text: "test"}
- name: input_text
params: {selector: "#password", text: "pass"}
- name: click
params: {selector: "button"}
- name: wait_for_element
params: {selector: ".welcome", timeout: 5}
- name: assert_text
params: {selector: ".welcome", expected: "欢迎"}把这个YAML丢给调度器,设置driver_type=web,跑Web。设置driver_type=app,跑App(只要selector能被Appium解析)。设置driver_type=api,框架会自动将input_text和click转换为HTTP请求(如果实现了对应映射)。实际上,接口环境不需要填表单,所以我们会为接口场景单独写一个更简洁的技能序列。但关键在于:测试人员不需要记住三套API,只需要记住技能名字和参数。
可以被截图传播的观点句:
五、工程落地启示:你的测试资产不该绑定在某种工具上
这个框架开源后,已经在三个团队落地。总结三条最直接的经验。
启示一:把现有测试脚本拆成“技能库”和“用例层”
不要一次性重写所有用例。先从最常用的10个操作开始,注册成技能。然后让用例通过技能调用来重构。三个月后,你的用例文件会减少70%的重复代码。
启示二:技能可以跨项目共享
我们在框架里内置了一个远程技能仓库。团队A写的captcha_solver技能,团队B可以直接拉下来用。不需要复制代码,不需要知道内部实现。这对中大型团队的价值极大:你不再需要每个项目都配一个“自动化专家”。
启示三:新人培训周期从两周压缩到两天
新人只需要学会技能列表和YAML写法。不需要先学Playwright API,再学Appium,再学Requests。他们可以在第一天就写出能跑的用例,第二天理解技能背后的原理。
对在校生来说:你现在不需要纠结“学Web自动化还是App自动化”。你应该学的是“如何抽象测试技能”。这个能力在任何端都通用。
对初级工程师来说:试着把你平时写的Playwright脚本,重构为技能+用例的形式。你会发现自己开始从“写代码的人”变成“设计框架的人”。
对中级工程师来说:这个框架展示了如何用“注册中心+适配器”模式解耦测试工具。你可以把它推广到你的团队,或者自己实现一个更轻量的版本。
六、问你的团队一个问题
去你团队的自动化代码仓库里,随便找一个跨端的场景(比如用户从注册到下单)。统计一下:为了支持Web、App、接口三种环境,你的代码里重复实现了多少遍“等待元素出现”“输入文本”“点击按钮”?然后问自己:如果明天要换掉其中一个底层框架(比如从Playwright换成Cypress),你要改多少处代码?
如果答案是“超过10处”,那么这个框架就值得你花一天时间研究。
