CSV + YAML 怎么描述测试:H5 SDK 自动化框架的数据模型设计

摘要

上一篇文章里,我从两个 Playwright 脚本讲起:

第一个脚本,用来验证不同设备环境下,首次游客登录是否会生成不同游客。

第二个脚本,用来验证点击 SDK 页面按钮之后,是否真的发出了正确的网络请求,并且请求参数和响应数据是否符合预期。

这两个脚本都能解决具体问题,但继续往下做时,我发现一个更大的问题:

text 复制代码
设备参数写死在 Python 里
页面元素写死在 Python 里
接口地址写死在 Python 里
断言逻辑写死在 Python 里
用例依赖写死在 Python 里
变量提取也写死在 Python 里

所以我开始把这套 H5 SDK 自动化测试从单脚本,重构成数据驱动框架。

这篇文章继续往下讲一个更具体的问题:

CSV + YAML 到底怎么描述一条自动化测试?

我的目标不是把所有字段逐个解释成说明书,而是讲清楚:

为什么第一版框架要把测试数据拆成 devices.csvcases.csvcase_steps.csvelements.yaml,它们分别解决什么问题,又为什么不能全部塞进一个文件里。


一、问题不是"用什么文件",而是"变化在哪里"

很多时候,一说数据驱动,大家马上会讨论:

text 复制代码
用 CSV 还是 Excel?
用 YAML 还是 JSON?
要不要用数据库?
要不要做用例管理平台?

但我这次设计数据模型时,最先考虑的不是文件格式。

我先问的是另一个问题:

这套 H5 SDK 自动化测试里,哪些东西会经常变化?

如果一套自动化脚本一直只有一条用例、一个浏览器、一个接口、一个页面按钮,那确实没必要做复杂设计。

但 H5 SDK 测试不是这样。

它天然会变化。


1.1 设备会变

今天我要模拟 Windows Chrome。

明天可能要模拟 iPhone Safari。

后面可能还要补:

text 复制代码
不同浏览器
不同屏幕尺寸
不同语言
不同时区
不同移动端特征
不同触摸能力

如果这些设备参数一直写在 Python 脚本里,那么每次新增设备,都要改代码。


1.2 用例会变

第一阶段我先做游客登录。

但 H5 SDK 后续还要覆盖:

text 复制代码
初始化
游客登录
邮箱登录
Google 登录
Facebook 登录
Apple 登录
SDK 埋点
自定义埋点
支付
归因
用户信息
账号绑定和解绑

如果每新增一个测试点都复制一个 Python 脚本,脚本数量会越来越多。


1.3 步骤会变

不同业务流程里,步骤不一样。

比如游客登录的步骤可能是:

text 复制代码
打开页面
等待 Channel ID
点击初始化
等待初始化接口
点击游客登录
等待登录接口
提取 userName
等待用户信息接口
等待登录成功埋点

但邮箱登录可能还要:

text 复制代码
输入邮箱
获取验证码
输入验证码
点击邮箱登录
等待邮箱登录接口
校验登录态

支付流程又完全不一样。

所以步骤本身也应该可以配置。


1.4 页面元素会变

测试页上的元素也可能变。

比如:

text 复制代码
初始化按钮 ID 变了
游客登录按钮 ID 变了
状态文案区域变了
操作日志区域结构变了
第三方登录按钮位置变了

如果选择器散落在 Python 代码和 CSV 步骤里,页面一改,就要到处搜索修改。


1.5 接口断言也会变

不同功能要断言的接口不同。

游客登录要断言:

text 复制代码
/test-api/oauth2/login
/test-api/oauth2/playergameuser/getUserById
/test-api/client/adjusteventrecord/save

初始化要断言:

text 复制代码
/test-api/client/init

埋点要断言:

text 复制代码
eventName
deviceId
channelId
sdkType
browser
os

支付要断言的字段会更多。

这些断言不应该都写死在 Python 里。


1.6 所以我先拆变化点

最后,我把第一版配置拆成了四份:

text 复制代码
data/devices.csv
data/cases.csv
data/case_steps.csv
data/elements.yaml

它们分别回答四个问题:

text 复制代码
用什么设备跑?       -> devices.csv
要跑哪些用例?       -> cases.csv
每条用例怎么执行?   -> case_steps.csv
页面元素怎么定位?   -> elements.yaml

这就是第一版数据模型的核心。


二、为什么不能只用一个大 CSV?

最直接的做法,是把所有东西都写进一个大 CSV。

比如一行里同时写:

text 复制代码
用例 ID
用例名称
模块
设备参数
页面地址
按钮选择器
接口地址
断言表达式
变量提取规则
状态文件
是否继续执行

看起来文件少了,管理起来好像也简单。

但真正写起来,很快会出现问题。


2.1 重复会非常多

同一个设备可能会被很多用例复用。

比如 device_a 可能用于:

text 复制代码
游客登录
邮箱登录
初始化
埋点
支付

如果每条用例、每个步骤都重复写:

text 复制代码
user_agent
viewport_width
viewport_height
locale
timezone_id
device_scale_factor
is_mobile
has_touch

CSV 会迅速膨胀。

更麻烦的是,一旦设备参数要改,就要改很多行。

这很容易漏。


2.2 修改风险会变大

比如页面上的游客登录按钮选择器原来是:

text 复制代码
#testGuestLogin

如果这个选择器出现在几十行步骤里,页面一改,就要逐行修改。

我更希望的是:

text 复制代码
步骤里只写 guest_login_button
真正的选择器统一放到 elements.yaml

这样按钮选择器变化时,只改一个地方。


2.3 不同层级的信息会混在一起

用例信息、设备信息、步骤信息、页面元素信息,其实不是同一类东西。

比如:

text 复制代码
case_id 属于用例层
device_id 属于设备层
action 属于步骤层
#initSDK 属于页面元素层

如果强行放在一个表里,短期看起来文件少了,长期会导致配置越来越难读。

最后很容易变成:

text 复制代码
一个巨大 CSV
什么都能写
什么都不好维护

所以我没有追求"文件越少越好"。

我更关注的是:

每个文件的职责是否清楚。


三、devices.csv:把设备参数从脚本里拿出来

第一个文件是 devices.csv

它解决的问题是:

同一套用例,可以在不同浏览器 / 不同设备环境下运行。


3.1 原来设备参数写在 Python 里

在最开始的多游客登录脚本里,设备参数大概是这样写的:

python 复制代码
BROWSER_PROFILES = [
    {
        "name": "windows_chrome",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
        "viewport": {"width": 1366, "height": 768},
        "locale": "zh-CN",
        "timezone_id": "Asia/Shanghai",
        "device_scale_factor": 1,
        "is_mobile": False,
        "has_touch": False,
    },
    {
        "name": "iphone_safari",
        "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...",
        "viewport": {"width": 390, "height": 844},
        "locale": "zh-CN",
        "timezone_id": "Asia/Tokyo",
        "device_scale_factor": 3,
        "is_mobile": True,
        "has_touch": True,
    },
]

这段代码能用。

但它的问题是:

text 复制代码
设备参数和测试逻辑绑在一起了。

如果我要新增一个设备,就要改 Python。

如果我要调整某个设备参数,也要改 Python。

但设备本身不是框架逻辑,它只是测试输入。

所以我把它抽到 devices.csv


3.2 devices.csv 示例

一行代表一个设备。

csv 复制代码
device_id,device_name,user_agent,viewport_width,viewport_height,locale,timezone_id,device_scale_factor,is_mobile,has_touch
device_a,Windows Chrome,"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",1366,768,zh-CN,Asia/Shanghai,1,false,false
device_b,iPhone Safari,"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",390,844,zh-CN,Asia/Tokyo,3,true,true

这样用例里只需要引用:

text 复制代码
device_a
device_b

不用再关心背后那一堆浏览器上下文参数。


3.3 devices.csv 字段说明

我第一版会先保留这些字段:

text 复制代码
device_id
device_name
user_agent
viewport_width
viewport_height
locale
timezone_id
device_scale_factor
is_mobile
has_touch

它们和 Playwright 创建 context 时的参数基本对应。

比如框架读取到一行设备配置后,可以转换成:

python 复制代码
context = browser.new_context(
    user_agent=device["user_agent"],
    viewport={
        "width": int(device["viewport_width"]),
        "height": int(device["viewport_height"]),
    },
    locale=device["locale"],
    timezone_id=device["timezone_id"],
    device_scale_factor=float(device["device_scale_factor"]),
    is_mobile=parse_bool(device["is_mobile"]),
    has_touch=parse_bool(device["has_touch"]),
)

这一步做完之后,设备配置就从 Python 代码里独立出来了。


3.4 这样拆的好处

第一,设备可以复用。

text 复制代码
游客登录可以用 device_a
邮箱登录可以用 device_a
埋点测试也可以用 device_a
支付流程也可以用 device_a

第二,新增设备不需要改框架代码。

只需要在 devices.csv 加一行。

第三,设备维度可以参与用例设计。

比如后续可以设计:

text 复制代码
同一用例在多个设备上执行
同一设备复用 storage_state
不同设备断言生成不同游客
不同语言环境验证页面和请求参数

这就是把设备从"脚本变量"变成"测试数据"带来的价值。


四、cases.csv:把"用例是什么"讲清楚

第二个文件是 cases.csv

它解决的问题是:

哪些是业务用例,以及这些用例之间有什么关系。


4.1 我不希望每个用例都是一个 Python 函数

如果不用数据驱动,最后很容易写成这样:

python 复制代码
def test_guest_001():
    ...

def test_guest_002():
    ...

def test_email_001():
    ...

def test_payment_001():
    ...

这种写法当然可以。

但问题是:

text 复制代码
业务用例仍然锁在 Python 代码里。

我更希望业务用例进入配置文件。

Python 只保留一个统一入口:

text 复制代码
pytest 启动
读取 cases.csv
选择要运行的 case_id
读取 case_steps.csv
交给框架执行

4.2 cases.csv 示例

当前游客登录相关用例可以先设计成这样:

csv 复制代码
case_id,module,case_name,enabled,depends_on,device_id,state_mode,state_file
GUEST_001,guest_login,新设备首次游客登录,true,,device_a,new,device_a_state.json
GUEST_002,guest_login,老设备再次游客登录,true,GUEST_001,device_a,load,device_a_state.json
GUEST_003,guest_login,不同设备首次游客登录,true,GUEST_001,device_b,new,device_b_state.json

这三条用例分别验证:

text 复制代码
GUEST_001:新设备可以创建游客
GUEST_002:老设备再次登录,应该还是同一个游客
GUEST_003:不同设备首次登录,应该生成不同游客

4.3 case_id:用例唯一标识

case_id 是用例的唯一标识。

它不只是一个名字。

它会被很多地方引用:

text 复制代码
命令行运行指定用例
case_steps.csv 绑定步骤
depends_on 声明用例依赖
日志记录
测试报告
变量作用域

所以 case_id 要稳定,不能随便改。

我一般会用模块前缀加编号,比如:

text 复制代码
GUEST_001
GUEST_002
EMAIL_001
PAY_001
EVENT_001

这样后续看日志和报告时会更清楚。


4.4 module:用例模块分组

module 用来做模块分组。

比如:

text 复制代码
guest_login
email_login
third_party_login
payment
event_tracking
init

这样后续就可以按模块执行:

bash 复制代码
python -m pytest tests/test_csv_runner.py --module guest_login

对于日常回归来说,这个字段很有用。

因为有时候我不想跑全部用例,只想跑某个模块。


4.5 enabled:是否默认启用

enabled 表示用例是否默认执行。

不是所有用例都适合默认跑。

比如:

text 复制代码
第三方登录可能依赖真实浏览器账号状态
支付可能依赖测试商品和测试账号
某些归因测试可能依赖外部链接和特定环境
某些破坏性测试不适合默认执行

这些用例可以先写进配置,但设置为:

text 复制代码
enabled = false

等需要时再通过命令行指定执行。


4.6 depends_on:用例依赖

depends_on 表示用例之间的依赖关系。

比如:

csv 复制代码
GUEST_002,guest_login,老设备再次游客登录,true,GUEST_001,device_a,load,device_a_state.json

这里表示:

text 复制代码
GUEST_002 依赖 GUEST_001

为什么?

因为 GUEST_002 要验证老设备再次登录。

它必须先有一个前置状态:

text 复制代码
GUEST_001 先用 device_a 登录
保存 device_a_state.json
保存 guest_a

然后 GUEST_002 才能加载这个状态,再次登录,并断言还是同一个游客。

如果我单独运行 GUEST_002,框架应该知道:

text 复制代码
它依赖 GUEST_001
需要先补跑 GUEST_001
或者提示依赖状态不存在

这比单纯依赖脚本执行顺序更清楚。


4.7 device_id:引用设备配置

device_id 表示这条用例使用哪个设备。

它引用的是 devices.csv 里的设备 ID。

比如:

text 复制代码
device_a
device_b

这样 cases.csv 不需要重复写设备参数。

框架执行时会:

text 复制代码
从 cases.csv 读取 device_id
再到 devices.csv 找对应设备参数
最后用这些参数创建 Playwright context

4.8 state_mode 和 state_file:控制浏览器状态

游客登录里,"新设备"和"老设备"的差异,很大一部分来自浏览器状态。

所以我设计了两个字段:

text 复制代码
state_mode
state_file

state_mode 可以先支持:

text 复制代码
new:新状态启动
load:加载已有状态

比如:

csv 复制代码
GUEST_001,guest_login,新设备首次游客登录,true,,device_a,new,device_a_state.json
GUEST_002,guest_login,老设备再次游客登录,true,GUEST_001,device_a,load,device_a_state.json

含义是:

text 复制代码
GUEST_001 用新状态启动,执行完保存 device_a_state.json
GUEST_002 加载 device_a_state.json,模拟老设备再次登录

这样"新设备"和"老设备"就不再靠 Python 逻辑写死,而是进入了用例配置。


五、case_steps.csv:一行描述一个测试步骤

第三个文件是 case_steps.csv

它是第一版框架里最重要的配置文件。

因为它真正描述了:

一条用例到底怎么执行。


5.1 为什么要一行一个步骤

一条自动化用例,本质上就是一组步骤。

比如游客登录:

text 复制代码
打开 SDK 测试页
等待 Channel ID
点击初始化
等待初始化成功
等待 init 接口
点击游客登录
等待 login 接口
提取 userName
等待 getUserById 接口
等待登录成功埋点接口
保存设备状态

这些步骤原来都写在 Python 里。

现在我希望它们变成配置。

所以 case_steps.csv 的基本思路是:

text 复制代码
一行 = 一个步骤

5.2 case_steps.csv 字段设计

第一版可以先保留这些字段:

text 复制代码
case_id
step_id
step_name
action
target
value
expect
save_as
depends_on_step
continue_on_fail

它们分别解决不同问题。

text 复制代码
case_id:这一步属于哪条用例
step_id:步骤编号
step_name:步骤名称,方便日志和报告展示
action:动作类型
target:操作目标,通常是元素别名或变量名
value:输入值、URL、接口关键字等
expect:预期结果或断言表达式
save_as:把结果保存成变量
depends_on_step:当前步骤依赖哪些前置步骤
continue_on_fail:失败后是否允许继续执行

5.3 action:框架要执行什么动作

action 是最核心的字段。

它决定这一行到底做什么。

第一版可以先支持这些动作:

text 复制代码
goto
click
fill
wait_element
wait_text
wait_request
extract
assert_var
save_state
load_state

比如:

csv 复制代码
case_id,step_id,step_name,action,target,value,expect,save_as,depends_on_step,continue_on_fail
GUEST_001,1,打开SDK测试页,goto,,https://sdk-test.uggamer.com/h5/test-sdk.html,,,,false
GUEST_001,2,等待Channel ID,wait_element,channel_id_input,,not_empty,,1,false
GUEST_001,3,点击初始化,click,init_button,,,,2,false

框架读到这些步骤后,会解释成真正的 Playwright 操作:

text 复制代码
goto -> page.goto()
wait_element -> locator 等待元素满足条件
click -> locator.click()

5.4 wait_request:把网络断言写进步骤

前面我已经封装过 NetworkRecorder

现在要做的是,把网络请求断言变成配置。

比如等待登录接口:

csv 复制代码
GUEST_001,7,等待登录接口,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true

这行虽然短,但表达了很多信息:

text 复制代码
它属于 GUEST_001
它是第 7 步
动作是 wait_request
等待的接口 URL 包含 /test-api/oauth2/login
断言 HTTP 状态码是 200
断言响应 code 是 200
断言 access_token 非空
把这次网络记录保存为 login_response
它依赖第 6 步
失败后允许继续执行后续不依赖它的步骤

以前这些逻辑要写在 Python 里:

python 复制代码
login_record = recorder.wait_for_url_contains("/test-api/oauth2/login")
assert_response_success(login_record)

login_response = login_record["response_json"]
access_token = login_response.get("data", {}).get("access_token")
assert access_token

现在它变成了 CSV 里的一行。

这就是数据驱动的核心价值。


5.5 一个游客登录用例的步骤示例

GUEST_001 为例,可以先写成这样:

csv 复制代码
case_id,step_id,step_name,action,target,value,expect,save_as,depends_on_step,continue_on_fail
GUEST_001,1,打开SDK测试页,goto,,https://sdk-test.uggamer.com/h5/test-sdk.html,,,,false
GUEST_001,2,等待Channel ID,wait_element,channel_id_input,,not_empty,,1,false
GUEST_001,3,点击初始化,click,init_button,,,,2,false
GUEST_001,4,等待初始化成功文案,wait_text,sdk_status,,SDK 初始化成功,,3,false
GUEST_001,5,等待初始化接口,wait_request,,/test-api/client/init,status=200;response.code=200,init_response,3,true
GUEST_001,6,点击游客登录,click,guest_login_button,,,,4,false
GUEST_001,7,等待登录接口,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true
GUEST_001,8,提取游客名称,extract,login_response,response.data.userName,,guest_a,7,true
GUEST_001,9,等待用户信息接口,wait_request,,/test-api/oauth2/playergameuser/getUserById,status=200;response.code=200,user_response,6,true
GUEST_001,10,等待登录成功埋点,wait_request,,/test-api/client/adjusteventrecord/save,status=200;response.code=200;request.eventName=sdk_登录成功,event_response,6,true
GUEST_001,11,保存浏览器状态,save_state,,device_a_state.json,,,6,true

这条用例就已经表达了:

text 复制代码
页面操作
页面断言
网络请求断言
请求参数断言
响应字段断言
变量提取
状态保存
失败策略

而这些都不需要写成一个新的 Python 测试函数。


六、depends_on_step:步骤之间也要有依赖

一条用例内部,步骤并不总是简单从上到下执行就完了。

尤其是在 SDK 测试里,我不希望某一个断言失败后,整条用例立刻结束。

我更希望一次执行能暴露更多信息。


6.1 为什么不能简单 fail-fast

比如游客登录后,我要检查三个请求:

text 复制代码
登录接口
查询用户接口
登录成功埋点接口

如果登录接口成功,但埋点接口没发,我希望测试报告能告诉我:

text 复制代码
登录接口成功
用户信息接口成功
埋点接口未捕获
最终用例失败

而不是第一个失败就直接停止。

但另一方面,如果"点击游客登录"这一步都失败了,那么后面的登录接口、用户信息接口、埋点接口就不应该继续等。

所以这里需要步骤依赖。


6.2 depends_on_step 的作用

depends_on_step 表示当前步骤依赖哪些前置步骤。

比如:

csv 复制代码
GUEST_001,7,等待登录接口,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true

这里的 depends_on_step = 6 表示:

text 复制代码
第 7 步依赖第 6 步

也就是必须先成功点击游客登录,才有必要等待登录接口。

如果第 6 步失败,那么第 7 步应该跳过。


6.3 框架执行时的判断逻辑

框架执行每一步时,可以先判断依赖步骤状态:

text 复制代码
如果依赖步骤成功:
    当前步骤继续执行

如果依赖步骤失败或跳过:
    当前步骤跳过

如果当前步骤失败:
    记录失败原因
    根据 continue_on_fail 判断是否继续后续步骤

这样就能做到:

text 复制代码
不是无脑失败就停
也不是失败后还盲目继续

而是根据步骤依赖来决定后续动作。


6.4 多个步骤依赖同一个动作

比如点击游客登录之后,会触发多个请求:

csv 复制代码
GUEST_001,6,点击游客登录,click,guest_login_button,,,,4,false
GUEST_001,7,等待登录接口,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true
GUEST_001,9,等待用户信息接口,wait_request,,/test-api/oauth2/playergameuser/getUserById,status=200;response.code=200,user_response,6,true
GUEST_001,10,等待登录成功埋点,wait_request,,/test-api/client/adjusteventrecord/save,status=200;response.code=200;request.eventName=sdk_登录成功,event_response,6,true

第 7、9、10 步都依赖第 6 步。

如果点击游客登录成功,它们都可以执行。

如果点击游客登录失败,它们都应该跳过。

这比单纯按顺序执行更合理。


七、save_as:把中间结果放进变量池

自动化测试里,很多断言不是孤立的。

前一个步骤拿到的结果,后一个步骤可能还要继续用。


7.1 为什么需要变量池

比如游客登录里,我需要验证:

text 复制代码
新设备首次登录生成 guest_a
老设备再次登录生成 guest_a_again
断言 guest_a_again == guest_a
不同设备首次登录生成 guest_b
断言 guest_b != guest_a

这就需要把前面步骤拿到的数据保存下来。

如果都写在 Python 里,就会变成各种函数参数和全局变量。

我更希望框架有一个统一的变量池。


7.2 save_as 保存步骤结果

save_as 字段用来保存中间结果。

比如:

csv 复制代码
GUEST_001,7,等待登录接口,wait_request,,/test-api/oauth2/login,status=200;response.code=200;response.data.access_token=not_empty,login_response,6,true

这一步会把完整的登录接口记录保存为:

text 复制代码
login_response

里面可以包含:

text 复制代码
请求 URL
请求方法
请求体
HTTP 状态码
响应 JSON
响应文本

后续步骤就可以继续使用它。


7.3 extract 从结果中提取变量

比如从登录响应里提取 userName

csv 复制代码
GUEST_001,8,提取游客名称,extract,login_response,response.data.userName,,guest_a,7,true

这行表达的是:

text 复制代码
从 login_response 里取 response.data.userName
保存为 guest_a

后续就可以通过:

text 复制代码
${guest_a}

来引用它。


7.4 跨用例变量比较

比如老设备再次登录:

csv 复制代码
GUEST_002,8,提取游客名称,extract,login_response,response.data.userName,,guest_a_again,7,true
GUEST_002,9,断言老设备游客不变,assert_var,guest_a_again,,eq:${guest_a},,8,true

这表示:

text 复制代码
提取当前登录返回的 userName,保存为 guest_a_again
断言 guest_a_again 等于 GUEST_001 中保存的 guest_a

不同设备登录则可以写成:

csv 复制代码
GUEST_003,8,提取游客名称,extract,login_response,response.data.userName,,guest_b,7,true
GUEST_003,9,断言不同设备游客不同,assert_var,guest_b,,not_eq:${guest_a},,8,true

这表示:

text 复制代码
device_b 生成的 guest_b 不应该等于 device_a 生成的 guest_a

7.5 变量池不能滥用

变量池很有用,但也不能滥用。

如果变量越来越多,依赖越来越深,说明用例之间耦合可能过重。

比如一条用例需要依赖前面五条用例产生的十几个变量,这种设计就不太健康。

变量池应该服务于明确的业务链路,比如:

text 复制代码
新设备 -> 老设备
创建订单 -> 查询订单
登录 -> 查询用户信息
初始化 -> 埋点上报

而不是让所有用例都互相依赖。


八、elements.yaml:把页面选择器和测试步骤解耦

第四个文件是 elements.yaml

它解决的问题是:

页面元素选择器不应该直接散落在测试步骤里。


8.1 原来选择器写在代码里

比如:

python 复制代码
page.locator("#channelIdInput")
page.locator("#initSDK")
page.locator("#sdkStatus")
page.locator("#testGuestLogin")

或者写在 CSV 里:

csv 复制代码
GUEST_001,3,点击初始化,click,#initSDK,,,,
GUEST_001,6,点击游客登录,click,#testGuestLogin,,,,

这两种方式都能用。

但维护性不够好。


8.2 用元素别名代替选择器

我更希望步骤里写的是:

csv 复制代码
GUEST_001,3,点击初始化,click,init_button,,,,
GUEST_001,6,点击游客登录,click,guest_login_button,,,,

而真实选择器放在 elements.yaml

yaml 复制代码
sdk_test_page:
  channel_id_input: "#channelIdInput"
  init_button: "#initSDK"
  sdk_status: "#sdkStatus"
  guest_login_button: "#testGuestLogin"
  google_login_button: "#testGGLogin"
  facebook_login_button: "#testFBLogin"
  apple_login_button: "#testAPPLELogin"
  operation_log: "#operationLog"

这样测试步骤就更接近业务语义。

text 复制代码
click init_button

比:

text 复制代码
click #initSDK

更容易理解。


8.3 elements.yaml 的好处

第一,步骤更可读。

测试用例里看到 guest_login_button,基本就知道这是游客登录按钮。

第二,选择器变更影响更小。

如果初始化按钮从:

text 复制代码
#initSDK

改成:

text 复制代码
button[data-testid="init-sdk"]

只需要改 YAML:

yaml 复制代码
init_button: 'button[data-testid="init-sdk"]'

不用改所有测试步骤。

第三,后续可以按页面或弹窗分组。

比如:

yaml 复制代码
sdk_test_page:
  channel_id_input: "#channelIdInput"
  init_button: "#initSDK"
  guest_login_button: "#testGuestLogin"

google_login_popup:
  email_input: 'input[type="email"]'
  next_button: "#identifierNext"
  password_input: 'input[type="password"]'

第三方登录后续会涉及 OAuth 弹窗,这种分组会很有用。


8.4 选择器策略也要治理

不过,elements.yaml 只是把选择器集中管理,并不能解决所有页面不稳定问题。

如果页面 DOM 本身频繁变化,还是需要更稳定的定位策略。

比如优先使用:

text 复制代码
稳定 ID
data-testid
data-test
aria role
明确文本
业务唯一属性

如果条件允许,最好和前端约定测试专用属性。

比如:

html 复制代码
<button data-testid="guest-login-button">游客登录</button>

这样自动化会比依赖复杂 CSS 路径更稳定。


九、四个文件如何串起来

这四个文件不是孤立的。

它们通过 ID 和别名串起来。

整体执行链路可以理解成这样:

text 复制代码
pytest 启动
  -> 读取 cases.csv
  -> 找到要执行的 case_id
  -> 根据 depends_on 解析用例依赖
  -> 根据 device_id 找到 devices.csv 中的设备
  -> 根据 case_id 找到 case_steps.csv 中的步骤
  -> 步骤执行时根据 target 查 elements.yaml
  -> 执行 Playwright 操作或网络断言
  -> 保存变量、状态和结果
  -> 最后交给 pytest 判定成功或失败

9.1 以 GUEST_001 为例

cases.csv 里有:

csv 复制代码
case_id,module,case_name,enabled,depends_on,device_id,state_mode,state_file
GUEST_001,guest_login,新设备首次游客登录,true,,device_a,new,device_a_state.json

框架读取后知道:

text 复制代码
要执行 GUEST_001
它属于 guest_login 模块
它使用 device_a
它是新状态启动
执行后可以保存 device_a_state.json

然后去 devices.csvdevice_a

csv 复制代码
device_id,device_name,user_agent,viewport_width,viewport_height,locale,timezone_id,device_scale_factor,is_mobile,has_touch
device_a,Windows Chrome,"Mozilla/5.0 ...",1366,768,zh-CN,Asia/Shanghai,1,false,false

框架用这些参数创建浏览器 context。

接着去 case_steps.csvGUEST_001 的所有步骤:

csv 复制代码
GUEST_001,1,打开SDK测试页,goto,,https://sdk-test.uggamer.com/h5/test-sdk.html,,,,false
GUEST_001,2,等待Channel ID,wait_element,channel_id_input,,not_empty,,1,false
GUEST_001,3,点击初始化,click,init_button,,,,2,false

执行第 2 步时,target = channel_id_input

框架就去 elements.yaml 找:

yaml 复制代码
channel_id_input: "#channelIdInput"

然后执行:

python 复制代码
page.locator("#channelIdInput")

这样四个文件就串起来了。


9.2 以 GUEST_002 为例

GUEST_002 是老设备再次登录。

它在 cases.csv 里可以这样写:

csv 复制代码
GUEST_002,guest_login,老设备再次游客登录,true,GUEST_001,device_a,load,device_a_state.json

这行的关键点是:

text 复制代码
depends_on = GUEST_001
device_id = device_a
state_mode = load
state_file = device_a_state.json

含义是:

text 复制代码
先确保 GUEST_001 已经执行过
复用 device_a
加载 device_a_state.json
再次游客登录
提取 guest_a_again
断言 guest_a_again == guest_a

这时框架不再靠脚本顺序猜测,而是根据 cases.csv 中的依赖关系执行。


9.3 这就是数据模型真正发挥作用的地方

单独看四个文件,好像只是拆配置。

但它们串起来之后,就能表达完整业务链路:

text 复制代码
用什么设备
跑哪条用例
是否依赖前置用例
是否加载浏览器状态
每一步做什么动作
操作哪个元素
等待哪个接口
断言哪个字段
保存哪个变量
最终如何判定成功或失败

这就是我设计 CSV + YAML 数据模型的目的。


十、第一版为什么不要设计得太复杂

第一版的数据模型,我刻意设计得比较克制。

它没有一开始就做:

text 复制代码
复杂条件表达式
复杂 JSONPath 语法
多环境配置中心
完整用例管理平台
大量业务关键字
复杂循环和分支
分布式执行

这是有意为之。


10.1 第一版先证明真实链路能跑通

第一版最重要的目标不是把所有未来能力一次性设计完。

而是先证明:

text 复制代码
这套模型能表达一条真实的 H5 SDK 回归链路。

游客登录这条链路已经覆盖了很多关键能力:

text 复制代码
设备差异
新老状态
用例依赖
页面操作
网络请求断言
变量提取
跨用例变量比较
状态保存和加载
失败后继续执行

如果这条链路能稳定跑通,后面扩展邮箱登录、第三方登录、埋点、支付,就有了基础。


10.2 过度设计会拖慢落地

如果第一版一开始就设计得太重,可能会出现几个问题:

text 复制代码
配置变难写
框架变难调试
字段越来越多
还没跑通核心链路,就陷入设计细节

自动化框架最怕一开始就做成"大平台"。

结果是:

text 复制代码
目录很多
概念很多
配置很多
但真实业务链路还跑不稳

所以第一版应该先小而稳。

先让游客登录这条链路跑通。

再根据真实扩展需求,逐步加能力。


十一、这套 CSV + YAML 模型的边界

这套设计不是万能的。

它只是第一版 H5 SDK 自动化框架的数据模型。

它有自己的适用边界。


11.1 CSV 不适合复杂分支

CSV 很适合表达线性步骤。

比如:

text 复制代码
打开页面
点击按钮
等待接口
提取变量
断言变量

但如果后续出现大量:

text 复制代码
if / else
循环
动态重试
复杂条件判断
多路径分支

就不应该硬塞进 CSV。

更合适的方式是:

text 复制代码
把稳定业务流程封装成关键字
CSV 调用更高层动作
复杂逻辑留在 Python 框架里

比如:

csv 复制代码
GUEST_001,5,执行游客登录流程,guest_login_flow,,,,guest_result,4,false

这个 guest_login_flow 可以在 Python 里封装一组稳定动作。


11.2 YAML 不能解决所有页面不稳定问题

elements.yaml 可以集中管理选择器。

但如果页面 DOM 本身很不稳定,YAML 也救不了。

这时需要从定位策略上治理:

text 复制代码
尽量不用长 CSS 路径
尽量不用容易变化的层级结构
优先使用稳定 ID
优先使用 data-testid
必要时和前端约定测试属性

自动化稳定性不只是测试代码问题,也和页面可测试性有关。


11.3 变量池不能无限扩张

变量池适合跨步骤、跨用例传递运行时数据。

但如果变量越来越多,说明用例之间的耦合可能越来越严重。

比如:

text 复制代码
A 用例依赖 B 的变量
B 用例依赖 C 的变量
C 用例又依赖 D 的状态

这种链路会越来越难维护。

所以变量池应该用于明确、必要的业务链路,而不是让所有用例都共享一堆全局变量。


11.4 storage_state 要注意安全

Playwright 的 storage_state 很适合模拟老设备或已登录状态。

但状态文件里可能包含:

text 复制代码
cookies
localStorage
登录态 token
用户标识
业务缓存

所以这类文件不要随便提交到远程仓库。

可以考虑:

text 复制代码
加入 .gitignore
只在本地或 CI 临时生成
必要时定期清理
敏感环境不要复用真实账号状态

十二、小结

第一版框架的数据模型,本质上是在拆分变化。

我没有把所有配置塞进一个大 CSV,而是拆成四个文件:

text 复制代码
devices.csv
cases.csv
case_steps.csv
elements.yaml

它们分别解决不同问题:

text 复制代码
devices.csv      -> 用什么设备跑
cases.csv        -> 有哪些用例,以及它们之间是什么关系
case_steps.csv   -> 每条用例具体怎么执行
elements.yaml    -> 页面元素怎么定位

这四个文件共同完成了一件事:

把原来写死在 Python 脚本里的测试行为,拆成可以被维护、review、复用和扩展的数据。

这也是数据驱动自动化测试最关键的价值。

到这里,框架已经有了"剧本"。

但只有剧本还不够。

下一篇我会继续讲:

这些 CSV / YAML 配置是怎么被真正执行起来的?

从一行 CSV 到一次浏览器操作,关键字驱动执行引擎到底应该怎么设计?

相关推荐
码农小白AI7 小时前
AI报告审核加速融入自动化实验室:IACheck破解智能设备时代报告管理新挑战
运维·人工智能·自动化
utf8mb4安全女神7 小时前
克隆的虚拟机怎么更改ip地址
运维
万能的知了8 小时前
服务器托管 vs 云主机 vs 裸金属:一个决策故事
运维·服务器·云计算
杨云龙UP9 小时前
Oracle RAC / ODA 生产环境指定 PDB 启动 SOP
linux·运维·数据库·oracle
郑洁文9 小时前
基于网络爬虫的Web敏感信息泄露自动化检测工具
前端·爬虫·网络安全·自动化
luweis9 小时前
企智孪生 ETA(3.3 认知算法层:ETA 的思维内核 3.4 基础架构:算力与弹性)【浙江联保网络 卢伟舜】
大数据·运维·线性代数·ai·矩阵·学习方法
极客老王说Agent9 小时前
屏幕理解能力是下一代自动化的关键吗?2026年自动化范式演进深度解析
运维·人工智能·ai·chatgpt·自动化
LT101579744410 小时前
2026年电商RPA选型指南:电商运营全流程自动化测评
运维·自动化·rpa
Black蜡笔小新10 小时前
自动化AI算法训练服务器DLTM训推一体化平台助力农业生产管理实现安全智能化
人工智能·算法·自动化
JAVA社区10 小时前
Java高级全套教程(十一)—— Kubernetes 超详细企业级实战详解
java·运维·微服务·容器·面试·kubernetes