CSV + YAML 怎么描述测试:自动化框架的数据模型设计
CSV + YAML 怎么描述测试:自动化框架的数据模型设计
摘要
前面三篇文章讲的是框架出现之前的过程。
我先用 Playwright 脚本验证了多设备游客登录,又用另一个脚本验证了网络请求捕获与断言。后来发现,如果继续复制脚本,设备参数、页面元素、接口断言、变量提取和用例依赖都会越来越难维护。
所以第一版框架的第一步,不是马上写复杂执行引擎,而是先设计测试数据应该怎么描述。
这篇文章讲的就是:为什么我把配置拆成 devices.csv、cases.csv、case_steps.csv 和 elements.yaml。
问题不是用 CSV 还是 YAML,而是变化在哪里
做数据模型之前,我先问的是:
哪些东西经常变?
哪些东西应该让测试同学维护?
哪些东西不应该写死在 Python 框架里?
H5 SDK 自动化里,变化点很多。
设备会变。
今天测 Windows Chrome,明天可能要测 iPhone Safari,后面还可能测不同语言、时区、分辨率和浏览器启动方式。
用例会变。
游客登录只是开始,后面还有初始化、邮箱登录、Google 登录、Facebook 登录、Apple 登录、埋点、支付。
步骤会变。
不同流程的页面操作、等待条件、接口断言、变量提取都不同。
页面元素会变。
测试页上的按钮 ID、输入框选择器、状态区域,都可能随着页面调整而变化。
如果这些都写在 Python 里,每次变化都要改代码。
所以我把第一版配置拆成四份:
data/devices.csv
data/cases.csv
data/case_steps.csv
data/elements.yaml
它们分别回答四个问题:
用什么设备跑? -> devices.csv
要跑哪些用例? -> cases.csv
每条用例怎么执行? -> case_steps.csv
页面元素怎么定位? -> elements.yaml
为什么不是一个大 CSV
最直接的做法,是把所有东西都放进一个大 CSV。
一行里写:
用例 ID
用例名称
设备参数
页面地址
按钮选择器
接口路径
断言字段
预期值
文件看起来少了,但维护会很快变差。
第一,重复太多。
同一个设备可能被很多用例复用。如果每个步骤都重复写 user agent、viewport、locale,CSV 会很快膨胀。
第二,修改风险大。
如果 #testGuestLogin 改成了 #guestLoginButton,我不希望在几十行步骤里逐个改。
第三,层级混乱。
设备参数、用例信息、步骤动作、页面选择器不是同一类信息。强行放在一个表里,短期省文件,长期难读。
所以我没有追求“文件越少越好”。
我更关心每个文件的职责是否清楚。
devices.csv:描述浏览器和设备环境
第一个文件是 devices.csv。
它解决的问题是:
同一套测试步骤,要在什么浏览器 / 设备环境下运行?
从脚本阶段看,设备参数原来可能是这样:
context = browser.new_context(
user_agent="Mozilla/5.0 ... Chrome/147.0.0.0 Safari/537.36",
viewport={"width": 1366, "height": 768},
locale="zh-CN",
timezone_id="Asia/Shanghai",
device_scale_factor=1,
is_mobile=False,
has_touch=False,
)
这些参数不是框架逻辑,而是测试数据。
所以我把它们放到 CSV:
device_id,device_name,user_agent,viewport_width,viewport_height,locale,timezone_id,device_scale_factor,is_mobile,has_touch,launch_mode
device_a,Windows Chrome,"Mozilla/5.0 ... Chrome/147.0.0.0 Safari/537.36",1366,768,zh-CN,Asia/Shanghai,1,false,false,isolated
device_b,iPhone Safari,"Mozilla/5.0 ... Mobile/15E148 Safari/604.1",390,844,zh-CN,Asia/Tokyo,3,true,true,isolated
框架读取后,会变成 DeviceConfig:
@dataclass(frozen=True)
class DeviceConfig:
device_id: str
device_name: str
user_agent: str
viewport_width: int
viewport_height: int
locale: str
timezone_id: str
device_scale_factor: float
is_mobile: bool
has_touch: bool
launch_mode: str = "isolated"
后面创建浏览器 context 时,再转换成 Playwright 参数:
def context_options(self) -> dict:
options = {}
options["user_agent"] = self.user_agent
options["viewport"] = {
"width": self.viewport_width,
"height": self.viewport_height,
}
options["locale"] = self.locale
options["timezone_id"] = self.timezone_id
options["device_scale_factor"] = self.device_scale_factor
options["is_mobile"] = self.is_mobile
options["has_touch"] = self.has_touch
return options
这样新增设备时,优先改 CSV,而不是改 Python。
cases.csv:描述用例是什么
第二个文件是 cases.csv。
它回答的是:
有哪些用例?
属于哪个模块?
是否启用?
依赖哪条用例?
用哪个设备?
是否加载浏览器状态?
有哪些用例级变量?
真实配置类似:
case_id,module,case_name,enabled,depends_on,device_id,state_mode,state_file,case_vars
GUEST_001,guest_login,new device creates guest,false,,device_a,new,device_a_state.json,channel_id=83b8...
GUEST_002,guest_login,same device keeps guest,false,GUEST_001,device_a,load,device_a_state.json,channel_id=83b8...
GUEST_003,guest_login,different device creates different guest,false,GUEST_001,device_b,new,device_b_state.json,channel_id=83b8...
这里已经能表达游客登录的业务关系:
GUEST_001:新设备创建游客。
GUEST_002:依赖 GUEST_001,加载同一状态,验证还是同一个游客。
GUEST_003:依赖 GUEST_001,换设备,验证不是同一个游客。
对应的数据对象是:
@dataclass(frozen=True)
class CaseConfig:
case_id: str
module: str
case_name: str
enabled: bool
depends_on: tuple[str, ...]
device_id: str
state_mode: str
state_file: Optional[str]
case_vars: Optional[str] = None
这一步的关键不是字段多不多,而是把“用例级信息”从步骤里拆出来。
比如 device_id 属于整条用例,不应该每个步骤重复写。
depends_on 也是用例关系,不应该藏在 Python 脚本顺序里。
case_steps.csv:描述每条用例怎么执行
第三个文件是 case_steps.csv。
它是整个数据模型里最像“剧本”的部分。
一行就是一个步骤:
case_id,step_id,step_name,action,target,value,expect,save_as,depends_on_step,continue_on_fail
GUEST_001,1,open test page,goto,,https://sdk-test.uggamer.com/h5/test-sdk.html,,,,true
GUEST_001,2,fill channel id,fill,channel_id_input,${channel_id},,,1,true
GUEST_001,3,click init,click,init_button,,,,2,true
GUEST_001,5,wait init api,wait_request,,/test-api/client/init,status=200;response.code=200,init_response,3,true
这些字段的含义是:
case_id -> 属于哪条用例
step_id -> 第几步
step_name -> 步骤名称,方便日志阅读
action -> 做什么动作
target -> 操作目标,比如元素别名或变量名
value -> 输入值,比如 URL、接口路径、填入文本
expect -> 预期,比如文本、接口断言表达式
save_as -> 把结果保存成哪个变量
depends_on_step -> 当前步骤依赖哪些步骤
continue_on_fail -> 失败后是否继续
对应的数据对象是:
@dataclass(frozen=True)
class StepConfig:
case_id: str
step_id: int
step_name: str
action: str
target: str
value: str
expect: str
save_as: str
depends_on_step: tuple[int, ...]
continue_on_fail: bool
这个设计的核心是 action。
它让 CSV 可以描述动作语义:
goto
fill
click
wait_text
wait_request
extract
assert_var
save_state
下一篇第 5 篇会专门讲:这些 action 是怎么被 StepRunner 翻译成真实浏览器操作的。
这一篇先记住一点:case_steps.csv 不是随便堆字段,它是在描述“用例执行剧本”。
elements.yaml:把页面选择器集中管理
第四个文件是 elements.yaml。
它解决的问题是页面元素定位。
如果直接在 CSV 里写:
click,#initSDK
短期没问题。
但如果页面选择器改了,就要去所有 CSV 步骤里找。
所以我更希望 CSV 写业务别名:
click,init_button
真正的选择器放在 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"
这样有几个好处:
CSV 更接近业务流程。
页面选择器集中维护。
元素命名可以统一。
页面改版时影响范围更小。
后面执行 click,init_button 时,框架会先从 elements.yaml 找到:
init_button -> #initSDK
再调用 Playwright 点击。
为什么 CSV 和 YAML 混用
我没有把所有配置都做成 CSV,也没有全部做成 YAML。
原因是它们适合表达的东西不一样。
CSV 适合表格型数据:
设备列表
用例列表
步骤列表
这些数据天然是一行一条记录。
YAML 更适合层级映射:
页面分组
元素别名
选择器
比如:
sdk_test_page:
init_button: "#initSDK"
比 CSV 更自然。
所以这里不是为了技术偏好混用,而是让数据格式匹配数据形状。
csv_loader 把文件变成对象
配置文件只是第一步。
Python 框架还要把它们读进来。
framework/config/csv_loader.py 里有三个加载函数:
def load_devices(path: Path = DATA_DIR / "devices.csv") -> dict[str, DeviceConfig]:
...
def load_cases(path: Path = DATA_DIR / "cases.csv") -> dict[str, CaseConfig]:
...
def load_steps(path: Path = DATA_DIR / "case_steps.csv") -> dict[str, list[StepConfig]]:
...
以步骤加载为例:
step = StepConfig(
case_id=row["case_id"].strip(),
step_id=int(row["step_id"]),
step_name=row["step_name"].strip(),
action=row["action"].strip(),
target=row["target"].strip(),
value=row["value"].strip(),
expect=row["expect"].strip(),
save_as=row["save_as"].strip(),
depends_on_step=_split_ints(row["depends_on_step"]),
continue_on_fail=_as_bool(row["continue_on_fail"]),
)
这一步会把 CSV 字符串转换成更明确的配置对象。
后面的执行器就不用直接处理原始 CSV,而是处理 CaseConfig、DeviceConfig、StepConfig。
这也是数据模型的意义:不是为了多几个类,而是让框架里的每层逻辑都有清楚输入。
这套数据模型的边界
第一版数据模型不是万能的。
它适合描述线性的、步骤化的自动化流程。
比如:
打开页面
填写参数
点击按钮
等待接口
提取字段
断言变量
保存状态
但如果出现非常复杂的逻辑,比如大量循环、动态分支、复杂数据构造,就不应该硬塞进 CSV。
这时更好的做法是:
把复杂逻辑沉淀成新的关键字。
CSV 只负责调用这个关键字。
也就是说,CSV 不应该变成另一种编程语言。
它应该描述测试意图。
Python 框架负责实现执行细节。
小结
这一篇要记住四个文件:
devices.csv -> 设备和浏览器环境
cases.csv -> 用例元信息、依赖、状态、变量
case_steps.csv -> 步骤剧本
elements.yaml -> 页面元素别名和选择器
它们共同完成了一件事:
把经常变化的测试信息,从 Python 脚本里移到数据文件里。
这就是数据驱动的第一步。
但数据文件本身不会执行测试。
CSV 里写了一行 click,浏览器为什么真的会点按钮?
CSV 里写了一行 wait_request,框架为什么真的能等接口并断言?
下一篇就进入执行层,讲关键字驱动执行引擎是怎么把一行 CSV 变成一次真实浏览器操作的。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)