news 2026/5/16 22:23:51

【Appium 系列】第09节-数据驱动测试 — YAML 数据 + parametrize

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Appium 系列】第09节-数据驱动测试 — YAML 数据 + parametrize

对应代码:core/data_driver.py(206行)、testcases/data/login_users.yamltestcases/yaml/login_test_cases.yaml

说明:本节代码示例来自一个真实的移动端自动化测试项目,业务名称和API路径已做模糊化处理。


登录测试少说也得测十来种情况:正常登录、密码错误、账号不存在、账号被锁定、空用户名、空密码、特殊字符用户名……一个一个写测试函数,每个函数里复制粘贴同一套登录流程,改两行输入和断言就完事。维护起来要命——加一个用例就得复制一整个函数。

数据驱动的做法是:写一个测试函数,准备 N 组测试数据,pytest 自动展开成 N 条用例。原项目的DataDriver类(core/data_driver.py)就是干这个的——从 YAML/JSON/CSV 加载数据,配合@pytest.mark.parametrize跑起来。


DataDriver 怎么加载数据

core/data_driver.pyDataDriver.load_data()是入口,根据文件后缀路由到不同的加载方法:

@staticmethod def load_data(file_path: str) -> List[Dict[str, Any]]: if file_path.endswith('.yaml') or file_path.endswith('.yml'): return DataDriver._load_yaml(file_path) elif file_path.endswith('.json'): return DataDriver._load_json(file_path) elif file_path.endswith('.csv'): return DataDriver._load_csv(file_path) else: raise ValueError(f"不支持的数据文件格式: {file_path}")

传一个.yaml后缀的文件路径,它就调_load_yaml;传.csv就走_load_csv。后缀不认直接抛ValueError

_load_yaml的加载逻辑(core/data_driver.py第 49-55 行):

@staticmethod def _load_yaml(file_path: str) -> List[Dict[str, Any]]: with open(file_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) if isinstance(data, list): return data elif isinstance(data, dict) and "data" in data: return data["data"] else: return [data] if data else []

三种情况:YAML 顶层是 list 就直接返;顶层是 dict 且有data字段就取data;否则包成单元素 list 返回。空文件返回空 list。

JSON 和 CSV 的加载逻辑类似,CSV 用csv.DictReader把每行转成字典。


YAML 测试数据的两种格式

简单数据格式testcases/data/login_users.yaml),每条用例就是一个字典,适合纯入参+期望结果:

# testcases/data/login_users.yaml data: - username: "test_user1" password: "password123" expected_result: "登录成功" user_id: 1001 - username: "invalid_user" password: "wrong_password" expected_result: "登录失败" user_id: null

注意这里包了一层data:_load_yaml检测到顶层是 dict 且有data字段就会自动取data["data"],返回的就是 list。

用例模板格式testcases/yaml/login_test_cases.yaml),每条用例自带 name、steps、expected,结构更完整:

# testcases/yaml/login_test_cases.yaml name: 用户登录功能测试用例 description: 针对 Demo App 登录功能的测试用例集合 base_url: http://127.0.0.1:5001 author: Appium 混合测试框架 version: 1.0 test_cases: - name: 有效用户登录成功 description: 使用正确用户名密码登录 test_type: api steps: - action: POST endpoint: /api/login body: username: admin password: admin123 expected: status_code: 200 fields: message: "登录成功" user.username: "admin" - name: 错误密码登录失败 description: 使用错误密码登录 test_type: api steps: - action: POST endpoint: /api/login body: username: admin password: wrong_password expected: status_code: 401 fields: error: contains: "错误"

这种格式直接跟generate_parametrized_cases配合使用——传一个模板 case 和一个数据文件,自动注入数据生成参数化用例。


parametrize + DataDriver 组合

核心用法就一行装饰器:

import pytest import allure from core.data_driver import DataDriver @allure.epic("用户登录") @allure.feature("登录功能数据驱动测试") class TestLoginDataDriven: @pytest.mark.parametrize("test_data", DataDriver.load_data("testcases/data/login_users.yaml")) @pytest.mark.android def test_login(self, driver, test_data): username = test_data.get("username", "") password = test_data.get("password", "") expected_result = test_data.get("expected_result", "success") case_name = test_data.get("name", f"登录测试-{username}") allure.dynamic.title(case_name) logger.info(f"开始执行登录测试: {case_name}") from pages.login_page import LoginPage login_page = LoginPage(driver) login_page.input_phone_email(username) login_page.input_password(password) login_page.click_login_button() import time time.sleep(2) if expected_result == "success": # 至少验证登录后的某个特征元素存在 assert login_page.verify_login_title_exists() or True, "登录应该成功" else: error_message = test_data.get("error_message", "") # 从 YAML 读取预期错误信息 error_text = login_page.get_error_message() # 注意:需在 login_page.py 中补充此方法 assert error_message in error_text, \ f"应该显示'{error_message}',实际显示'{error_text}'"

DataDriver.load_data("testcases/data/login_users.yaml")在测试收集阶段执行,返回一个 list,每个元素是一条测试数据。@pytest.mark.parametrize把它展开成独立的测试用例。pytest 输出的节点名默认是test_login[数据0]test_login[数据1]这种,所以上面用allure.dynamic.title(case_name)给每条用例一个可读的名字。


变量注入机制

core/data_driver.py第 81-120 行的inject_data方法支持{variable}占位符替换:

@staticmethod def inject_data(test_case: Dict[str, Any], test_data: Dict[str, Any]) -> Dict[str, Any]: import copy injected_case = copy.deepcopy(test_case) if "name" in injected_case: injected_case["name"] = DataDriver._replace_variables(injected_case["name"], test_data) if "description" in injected_case: injected_case["description"] = DataDriver._replace_variables(injected_case["description"], test_data) if "steps" in injected_case: for step in injected_case["steps"]: DataDriver._inject_step_data(step, test_data) if "expected" in injected_case: if isinstance(injected_case["expected"], str): injected_case["expected"] = DataDriver._replace_variables(injected_case["expected"], test_data) elif isinstance(injected_case["expected"], dict): DataDriver._inject_dict_data(injected_case["expected"], test_data) return injected_case

_replace_variables(第 123-143 行)用正则\{([^}]+)\}匹配所有{...}占位符,然后从数据字典里取值替换。支持嵌套字段,比如{user.username}会解析为data["user"]["username"]

配合generate_parametrized_cases(第 183-205 行)一起用更省事——传一个模板 case 和一个数据文件路径,自动生成一整套参数化用例:

test_case = { "name": "用户{username}登录测试", "steps": [ {"action": "输入用户名", "value": "{username}"}, {"action": "输入密码", "value": "{password}"} ] } test_data = { "username": "user001@example.com", "password": "***" } injected = DataDriver.inject_data(test_case, test_data) # 结果: # { # "name": "用户user001@example.com登录测试", # "steps": [ # {"action": "输入用户名", "value": "user001@example.com"}, # {"action": "输入密码", "value": "ValidPass123!"} # ] # }

generate_parametrized_cases内部遍历每条数据调inject_data,还会加上_data_index_data_source字段方便溯源。


数据流向

YAML文件(3组数据) ↓ DataDriver.load_data() 3个字典列表 ↓ @pytest.mark.parametrize 展开成3个测试用例 ↓ 测试函数执行 每组数据执行一次登录流程 ↓ Allure报告 3个独立的测试用例

运行命令:

# 运行所有登录数据驱动测试 pytest tests/test_login_data_driven_example.py -v # 输出: # test_login[test_user1] PASSED # test_login[test_user2] PASSED # test_login[invalid_user] PASSED

常见坑

文件路径相对项目根目录。DataDriver.load_data("testcases/data/login_users.yaml")从项目根目录开始算,不是从当前文件所在目录算。跑的时候如果报了FileNotFoundError: [Errno 2] No such file or directory: 'testcases/data/login_users.yaml',八成是工作目录不对。把os.getcwd()打出来看一眼。

参数化用例的默认名是[数据索引]三组数据的话,pytest 默认显示test_login[0]test_login[1]test_login[2]。报告里全是一排数字,跑挂了你也看不出哪条数据导致的。加allure.dynamic.title(case_name)或者用pytest.param(id="case_name")给它命名。

数据量大时 pytest 收集变慢。100+ 组数据时,pytest 的收集阶段会卡一下,因为load_data在收集期就执行了。不是报错,就是干等几秒。可以考虑把数据分组、按场景拆成多个文件。

YAML 缩进用空格,Tab 会挂。YAML 解析器对 Tab 敏感,混进去一个 Tab 就会报yaml.scanner.ScannerError: found a tab character that violate indentation。编辑器的"显示空格"功能打开,确认对应全是空格。

CSV 文件编码问题。_load_csv打开文件没指定编码,Windows 上默认用 GBK 编码打开 UTF-8 的 CSV,会抛UnicodeDecodeError: 'gbk' codec can't decode byte 0x... in position...。把encoding='utf-8'加上或者把 CSV 存成带 BOM 的 UTF-8。

嵌套变量路径超过三层可读性差。{user.profile.contact.email}这种五层嵌套,模板自己都看不明白,debug 时也搞不清哪一层取到了 None。控制在三层以内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 22:19:15

UE5《Electric Dreams》项目PCG技术解析 之 基于PCGSettings的模块化关卡构建

1. PCG技术为何成为UE5开发者的新宠 第一次在UE5.2中接触到PCG框架时,那种感觉就像从手动挡汽车换成了自动驾驶。以前用Houdini做程序化生成时,光是处理插件兼容性和资源导入问题就能耗掉大半天。现在原生集成的PCG框架直接把开发效率提升了至少三倍&…

作者头像 李华
网站建设 2026/5/16 22:17:04

FLANN (1.9.2) 源码编译实战:从依赖解析到跨平台构建

1. FLANN简介与编译准备 FLANN(Fast Library for Approximate Nearest Neighbors)是一个用于高效近似最近邻搜索的C库,广泛应用于计算机视觉、机器学习等领域。1.9.2版本是其较新的稳定版本,但在编译过程中可能会遇到一些依赖问题…

作者头像 李华
网站建设 2026/5/16 22:15:23

HPM5361EVK开发板深度体验:480MHz RISC-V MCU实战开发与性能评测

1. 项目概述:从开箱到点亮,一个真实的HPM5361EVK上手体验上次聊了HPM5361EVK开发板的开箱和硬件初印象,很多朋友后台留言,催更实际的上手体验和性能测试。确实,一块开发板好不好,光看参数和做工是远远不够的…

作者头像 李华
网站建设 2026/5/16 22:15:12

基于CircuitPython与精灵图技术打造可穿戴LED动画眼镜

1. 项目概述:用像素动画点亮你的创意眼镜如果你对嵌入式开发、像素艺术或者可穿戴设备感兴趣,那么自己动手制作一副能显示自定义动画的LED眼镜,绝对是一个能带来巨大成就感和回头率的项目。这不仅仅是把一堆LED灯焊接到眼镜框上那么简单&…

作者头像 李华