ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据驱动测试DDT实战:用Python与pytest解耦测试数据,告别重复脚本

数据驱动测试DDT实战:用Python与pytest解耦测试数据,告别重复脚本 开头讲一件我早年间干过的蠢事。那时候我刚接触自动化测试接了一个登录功能模块的测试任务产品经理给了整整三页测试点光账号密码的组合就有十几条正确的、密码错的、账号不存在的、大小写敏感、账户锁定的、验证码过期的各种情况。我当时的做法特别原始把每一条用例单独写成一个函数一个函数一个函数地复制粘贴改了输入参数和断言数值就算完事。代码看起来像老太太的裹脚布又臭又长大概写了三百多行跑一遍PASS了我心里还挺美。结果第四天产品改了需求密码错误提示从“账号或密码错误”改成了“密码错误请重新输入”我整个人是崩溃的因为这十几条用例里有过半的断言都跟这条提示相关我一条一条翻代码累了整整一个下午。从那以后我就长了记性开始研究怎么让一份测试脚本跑多组数据也就是今天要聊的DDT数据驱动测试。DDT全称是Data-Driven Testing中文叫数据驱动测试核心就一句话把测试数据从测试逻辑里剥离开同一份脚本喂不同的数据得到不同的用例结果。这个思路解决的不只是“代码冗余”的问题它真正改变的是测试用例的组织方式、可维护性和复用率。如果你正在做接口自动化、UI自动化或者你只是想把测试代码写得更像样子、减少返工这篇文章都值得你看完。我会从原理讲到实践从入门写法讲到进阶数据源最后再把实战里踩过的坑全部翻出来给你看。1. 先搞清楚数据驱动测试到底解决了什么问题1.1 没有数据驱动时的痛苦日常我继续讲登录这个例子。一个登录功能在没有数据驱动的情况下通常遇到三个痛点。第一个是代码重复。每条测试数据对应的用例脚本结构几乎是完全一样的都是输入用户名、输入密码、点击登录、断言提示信息但是因为输入值和预期值不同就得复制一份。我一个测试页面写了十几次click和send_keys代码量爆炸不说维护的时候随便动一个定位selector就得全文搜索替换漏掉一处就眼睁睁看着用例挂掉。第二个是数据和逻辑耦合严重。你把测试数据写死在代码里意味着每换一组数据就要改一次代码。这在敏捷迭代的环境下非常致命因为产品改数据比改逻辑频繁太多了。今天这个账号是测试号明天环境一刷新账号密码全部失效你得打开源代码去找这些写死的字符串改完还得提交代码走流程效率低得离谱。第三个是造数和覆盖率的矛盾。越到项目后期越需要大量的边界数据和异常数据来验证系统的健壮性比如手机号11位、密码最长20位、用户名含特殊字符、空值、超长字符串。这些数据单独写用例成本太高不写又怕漏测。于是在没有数据驱动的情况下很多人选择了“抽样”只测个正常场景和失败场景一旦线上出了问题复盘时才发现当初就是因为数据覆盖不够。这三个痛点其实是自动化测试从“demo能跑”过渡到“工程化可维护”的必经门槛。数据驱动并不是什么高深的理论它只是用一种更聪明的组织方式把测试脚本中的可变部分和不可变部分分开让重复劳动交给数据去做。1.2 数据驱动测试的两个核心原则数据驱动测试在我看来有两条核心原则弄懂了这两个原则你写出来的用例质量会有一个质的提升。原则一脚本逻辑与测试数据完全分离。脚本只负责动作比如打开页面、填写表单、调用接口、点击按钮数据负责“喂料”决定每次迭代时使用什么输入值、期望得到什么结果。数据可以放在代码里比如装饰器参数放在文件里JSON/YAML/CSV/Excel甚至放在数据库表里。关键是当你需要增加一条用例时理论上一行代码都不用改。原则二一条数据就是一个完整用例。理想情况下测试报告里每一条数据都应该对应一条独立的用例记录包括它的名称、输入、预期、实际、耗时和状态。这就要求数据驱动框架在运行时自动“展开”数据把每一组数据当成独立用例来收集和执行。如果十组数据跑下来只显示一条用例那数据的价值就打了折扣因为你无法快速定位是哪一组数据失败了。理解了这两个原则你就理解了DDT的骨架。后面我们要讲的DDT库、pytest参数化、数据文件加载统统都是围绕这两个原则在做文章。1.3 不是所有场景都适合硬套数据驱动我也见过一种反面典型不管什么用例硬往数据驱动上套结果代码变得极其晦涩。比如一个完整的业务链路中间有十几步操作每一步的页面跳转和元素都完全不同这种场景就不适合“一份脚本多组数据”的套路。更合适的是关键字驱动或者流程驱动把步骤也当成数据的一部分来组织。所以开头我得把边界说清楚数据驱动适合的是逻辑相同、流程相同、只有输入和预期不同的批量场景。典型代表包括接口参数校验、表单字段验证、搜索排序规则、权限矩阵验证。如果你的用例流程差异很大数据驱动能做的是把每个流程对应的数据抽出来做二次封装而不是硬把所有流程塞进同一个函数。2. 数据驱动测试的几种落地方式2.1 Python unittest DDT库最经典的组合聊到DDT实现Python生态里最经典、也最直接的就是unittest配合DDT库。这个库的名字就叫ddt从PyPI上直接pip install ddt就能装。它的用法非常直白。先导入然后在测试类上加上ddt装饰器再在具体的测试方法上用data装饰器传入多组数据。看下面这个最基本的小例子import unittest from ddt import ddt, data, unpack ddt class TestLogin(unittest.TestCase): data((admin, 123456, 登录成功), (admin, wrong, 账号或密码错误), (nobody, 123456, 账号或密码错误)) unpack def test_login(self, username, password, expected): result login_api(username, password) self.assertEqual(result, expected) if __name__ __main__: unittest.main(verbosity2)这里有几个关键点需要解释。ddt是给整个测试类做标记告诉框架“这个类里有数据驱动的用例”没有这个装饰器后面的data和unpack都不会生效。data后面传的参数就是你要喂给测试方法的数据可以是单个值、元组、字典甚至可以是别的对象。unpack的作用是把元组里的元素逐个拆开按顺序传给测试方法的参数。如果你不用unpack那整个元组会作为一个参数传入你需要自己在方法里做解包。这组代码运行时框架会把test_login方法执行三次每次传入一组不同数据报告里会显示三条用例名称后面还带序列号比如test_login_1、test_login_2、test_login_3。这就是刚才说的“一条数据就是一个用例”的实际体现。除了直接用dataDDT库还提供了file_data装饰器可以直接读取JSON或YAML文件里的数据。from ddt import ddt, file_data ddt class TestLogin(unittest.TestCase): file_data(login_data.json) def test_login(self, username, password, expected): result login_api(username, password) self.assertEqual(result, expected)这时候login_data.json的内容大概长这样{ case_1: {username: admin, password: 123456, expected: 登录成功}, case_2: {username: admin, password: wrong, expected: 账号或密码错误} }注意file_data对数据格式是有要求的最外层必须是字典字典里每个key对应一条用例value是传给测试方法的参数。如果value是字典框架会自动按参数名去匹配方法的形参如果value是列表/元组则按位置匹配。这个设计在实际工作中非常顺手因为带一个case名称失败报告上一眼就能看明白。有个小坑必须说file_data和unpack不能混用。file_data已经隐含了解包逻辑你再手动加unpack反而会报错。我第一次用的时候不知道来回折腾了好一阵子才发现问题。2.2 pytest.mark.parametrize更现代的替代方案如果你的项目用的是pytest框架那数据驱动的姿势就更多了。pytest内置了参数化功能不需要额外装DDT库用的就是pytest.mark.parametrize装饰器。import pytest pytest.mark.parametrize(username,password,expected, [ (admin, 123456, 登录成功), (admin, wrong, 账号或密码错误), (nobody, 123456, 账号或密码错误), ]) def test_login(username, password, expected): result login_api(username, password) assert result expected这里第一个参数是字符串里面用逗号分隔多个参数名第二个参数是一个列表列表里的每个元素就是一组测试数据。pytest运行时会把test_login扩展成三个测试用例用例ID后面会自动带上参数值的组合比如test_login[admin-123456-登录成功]。那DDT单不够用了吗并不是。DDT库在unittest生态里几乎是标准配置特别是老项目从unittest起步迁移成本最低的做法就是引入ddt库几十行改动就能完成数据驱动改造。而pytest本身就是为更好的测试体验设计的内置的参数化比DDT库的装饰器写法更灵活比如支持多组参数嵌套、支持参数化fixture、支持在参数列表中直接传fixture对象。我拿两个框架做个小对比对比项unittest DDTpytest parametrize安装依赖需要额外安装ddtpytest自带无需额外依赖数据来源data装饰器、file_data读JSON/YAMLparametrize装饰器、间接支持文件用例ID自动加序号不够直观自动拼参数值一眼看懂失败重跑依赖第三方插件配置繁琐pytest-rerunfailures一行搞定数据依赖无内置方案fixture可以处理复杂依赖生态集成较老适合历史项目新项目更推荐社区更活跃有一点要注意如果一个项目里同一个测试函数既要跑接口用例又要跑UI用例参数来源完全不同那建议分成两个测试函数来写不要把业务边界模糊在参数化里。2.3 数据源的选择从代码到文件再到数据库很多新手上来就把测试数据直接写在装饰器里像上面那些示例一样。这种方式适合数据量小、数据相对固定的场景比如只有几组边界值。但一旦数据量大到几十上百组数据开始频繁变动或者数据要由产品、运营、开发多方维护那就必须把数据从代码里挪出去。我从实际项目的角度把数据源的选型分成了四档第一档代码内联数据。适合10组以内、数据基本不变的场景。优点是最简单一个文件搞定缺点是一旦数据更新就要改代码提交。第二档JSON/YAML文件。这是我最推荐的起步方案。JSON通用性好Python、Java、JS都能读写YAML更易读适合不熟悉代码的同学也能维护。数据放在独立的测试数据目录下和脚本分开管理。用file_data或pytest的params配合读取即可。第三档Excel表格。适合需要跟业务人员协作的场景比如用例评审时大家打开Excel看数据一目了然。但Excel有一个没法回避的痛点一旦用例数量上了规模Excel的维护会变得非常痛苦而且容易出现单元格合并、格式错乱、混入全角字符等问题。我在实际项目里后来是把它当作“过渡方案”来看待的。第四档数据库/测试平台。适合几百条以上数据、需要动态生成数据、或者数据本身就在业务库里的场景。比如接口自动化测试测试数据直接从接口返回里提取或者从库里查询。这种方式的灵活度最高但复杂度也最高一般中小团队不建议一上来就上数据库方案先把文件方案跑通有了需要再升级。3. 完整示例一个登录接口的数据驱动测试3.1 接口与数据准备光讲概念不落地等于白讲。这一节我用一个完整的登录接口例子把数据驱动的开发流程走一遍。假设被测系统是一个标准的后端登录接口地址为/api/login接收POST请求参数是username和password。返回结果是一个JSON登录成功时返回{code: 0, msg: 登录成功, data: {token: xxx}}登录失败时返回{code: 1001, msg: 账号或密码错误}我先准备一份测试数据文件login_data.json{ case_login_success: { username: admin, password: 123456, expected_code: 0, expected_msg: 登录成功 }, case_wrong_password: { username: admin, password: wrong_pass, expected_code: 1001, expected_msg: 账号或密码错误 }, case_user_not_exist: { username: not_exist_user, password: 123456, expected_code: 1001, expected_msg: 账号或密码错误 }, case_empty_username: { username: , password: 123456, expected_code: 1002, expected_msg: 用户名不能为空 }, case_empty_password: { username: admin, password: , expected_code: 1003, expected_msg: 密码不能为空 } }注意看我的JSON结构设计。每个case有一个语义化的key比如case_login_success一眼就知道这条用例是干嘛的。value是一个字典字段名和测试方法的参数名一一对应。这样后面跑挂了报告里直接显示case命不需要再人肉猜测是哪一组数据。3.2 用DDT库实现测试脚本就清爽了import requests import unittest from ddt import ddt, file_data BASE_URL http://127.0.0.1:8000 ddt class TestLoginAPI(unittest.TestCase): file_data(login_data.json) def test_login_api(self, username, password, expected_code, expected_msg): payload {username: username, password: password} resp requests.post(f{BASE_URL}/api/login, jsonpayload) body resp.json() self.assertEqual(resp.status_code, 200) self.assertEqual(body[code], expected_code) self.assertEqual(body[msg], expected_msg) if __name__ __main__: unittest.main(verbosity2)这段代码跑起来unittest会把json里的case逐个展开最终生成5条测试记录。每条记录都是一个独立用例如果其中一组数据挂了其他4组依然会正常执行测试报告上能清晰看到是哪个case出了问题。这里我想多说一句断言的设计。很多新手写断言特别喜欢只判断code或者只判断msg我建议两个都断言。因为光判断code为0可能msg已经变了但你没发现光判断msg又可能完全没走接口验证逻辑。两个都验才是真的把接口结果“锁死”了。3.3 用例组织与报告效果上面这个例子数据量还小感受不到DDT的威力。我讲一个真实项目里的情况我们当时做一个支付订单查询接口查询维度有订单号、用户ID、商户号、支付渠道、时间范围每个维度又有正常值、空值、不存在的值、超长值、非法字符等组合一算下来光参数校验用例就有80多条。用传统写法80多条就是80多个方法每个方法里重复写请求和断言代码文件几千行跑一遍报告拉不到底。改造成数据驱动后所有数据集中在JSON文件里按“维度场景”命名脚本总共不到100行任何一条用例挂了报告上的用例ID直接指向JSON里的某个key定位问题从“翻代码”简化成“查数据”。而且DDT对CI/CD的友好度也高。因为每一条数据都是独立用例Jenkins集成跑完以后邮件报告里能直接展示失败用例的参数名称开发同学看到就能直接提工单。这在团队协作场景中价值特别大。3.4 进阶从Excel读取测试数据做接口测试时很多团队习惯用Excel维护用例那我顺手把Excel读取的常见写法也放上来。用openpyxl库读取Excelimport unittest from ddt import ddt, data, unpack from openpyxl import load_workbook def load_test_data_from_excel(file_path, sheet_name): wb load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] rows [] # 假设第一行是表头 for row in ws.iter_rows(min_row2, values_onlyTrue): if row[0] is None: continue rows.append(row) return rows ddt class TestLoginFromExcel(unittest.TestCase): data(*load_test_data_from_excel(login_cases.xlsx, login)) unpack def test_login(self, case_name, username, password, expected_code, expected_msg): payload {username: username, password: password} resp requests.post(f{BASE_URL}/api/login, jsonpayload) body resp.json() self.assertEqual(body[code], expected_code) if __name__ __main__: unittest.main()这里有个细节分享一下load_workbook默认会把Excel里的日期读成datetime对象把数字变成float或int把公式变成公式字符串。如果要读的是纯文本和数字建议加data_onlyTrue它会读取公式最后一次计算的结果而不是公式本体。还有data(*列表)这种写法是Python解包操作把列表里的每个元素变成data的一个参数。我见过有人直接data(load_test_data_from_excel(...))结果整个列表被当成一组数据传进去了然后unpack因为解包不了直接报错。这个问题网上能被搜到几百次我估计是DDT入门最常见的问题之一。4. 实战中容易踩的坑与排查技巧4.1 失败用例定位难有些同学用了DDT以后发现用例跑挂了报告上给的用例名是一个带序号的test_login_3但序号对应的到底是哪组数据还得自己数一遍。这个问题在数据量大的时候特别明显。我的解决办法是在断言信息里带上case名称。不管是DDT库还是pytest参数化都可以做到。pytest的写法是给每条数据最后一个元素作为case_name然后断言时带上import pytest pytest.mark.parametrize(case_name,username,password,expected, [ (正常登录, admin, 123456, 登录成功), (密码错误, admin, wrong, 账号或密码错误), ]) def test_login(case_name, username, password, expected): result login_api(username, password) assert result expected, f{case_name} 断言失败实际结果{result}这样当断言失败时pytest的输出里会直接包含“密码错误 断言失败”而不是让你自己去猜第几组数据挂了。这个习惯一定要养成别偷懒。4.2 一条数据异常影响后续执行这个问题主要出在有数据依赖的场景。比如你跑的下单流程第一组数据是正常创建订单第二组数据是重复创建同一个订单号你本意是验证“重复订单号会被拒绝”但如果第一组数据创建订单失败了第二组数据也会跟着失败报错信息还让你一头雾水。面对这种场景我建议两种处理方式一是用例隔离。每个case都准备独立的数据不要依赖上一个case的执行结果。比如造数据就用接口的setup方法直接调用创建接口生成不要寄希望于上一条用例已经创建好了。二是数据清理。在执行用例前做前置清理比如删除历史遗留的订单记录。虽然麻烦但对稳定性的提升非常明显。数据驱动的框架可以用setUp或fixture的setup来做这一步。4.3 Excel/CSV读取时的中文乱码与格式问题读CSV文件用到csv模块时要特别注意编码。CSV文件如果是从Excel另存的默认编码可能是gbk直接用Python默认的utf-8去读满屏全是乱码。import csv with open(data.csv, r, encodingutf-8) as f: rows list(csv.reader(f))如果发现乱码把encoding改成gbk或utf-8-sig。utf-8-sig是一种带BOM的utf-8编码用于处理Windows下生成的UTF-8文件特别有效。Excel里还有一个阴间问题数字列明明填的是字符串读出来变成float。比如手机号13800138000会被读成1.3800138e10再转int就成了完全不对的值。解决办法是Excel里把这一列设置成“文本”格式或者代码里读到后手动格式化成字符串。老老实实数一下位数做一下数据清洗这种问题只能靠经验兜底。4.4 数据驱动与用例爆炸数据驱动有一个很容易被忽视的副作用用例数量呈爆炸式增长。你给登录写了几十组数据给注册写了几十组数据给查询订单再写几十组数据全套回归跑下来用例数可能上千条。如果很多用例还是UI层的每一条跑起来都要启动浏览器、加载页面整个回归时间就会失控。我的建议是按执行级别分层管理数据。接口层的用例数据可以多特别是参数校验类跑得快成本低UI层的用例数据尽量精简只保留端到端的关键路径和重要异常路径不要试图把接口层的数据全量搬到UI层重复一遍。数据驱动是帮你提升效率的工具不是用来制造重复劳动的工具。4.5 数据维护的规范用数据驱动时间久了我慢慢总结出一套数据文件的管理规范。首先是命名规范case名称统一用“业务_场景_期望结果”的格式比如login_wrong_password_fail、order_create_expired_coupon_fail不要用case1、test1这种毫无信息量的名字。其次是字段规范每条数据只保留测试会用到的字段不要杂七杂八塞一堆无关内容也不要几套用例混用同一个数据文件。项目大一点后数据文件按模块创建目录结构比如test_data/api/login/、test_data/api/order/和测试脚本目录保持对应关系。最后是版本管理数据文件必须纳入Git等版本控制系统因为数据变更是高频操作有了版本历史才能追溯“是哪一次数据修改导致用例变红的”。千万不要图省事把数据文件放在临时目录回头出了问题根本找不回现场。5. 数据驱动测试在各个测试类型的落地经验5.1 接口自动化数据驱动的主场如果你问我哪个测试类型最适合用数据驱动我一定会告诉你接口自动化。接口测试天然逻辑稳定、输入输出清晰、执行速度快是数据驱动发挥价值最大的地方。在接口项目里我建议把参数校验类的用例全部数据化每个接口单独建一个JSON数据文件里面覆盖正常值、边界值、缺失字段、非法类型、超长字符串、空值、特殊字符等几类典型场景。比如查列表接口的分页参数page传0、传-1、传abc、传100万、不传这些都可以用参数化去覆盖。数据时可以用一个技巧把预期错误码和错误提示也放在数据里。比如一个接口对不同参数有不同的错误码传统的写法可能只断言HTTP 200或断言body里的某一段通过数据驱动可以做到“错误码错误提示业务状态码”三合一断言让测试的兜底能力大大增强。5.2 UI自动化用数据驱动要克制UI自动化和接口自动化不一样每一步操作都有元素定位、等待时间、网络波动这些变量执行不稳定是常态。UI层的数据驱动如果用力过猛用例数量一多执行时间和误报率都会暴涨。我的建议是UI层跑数据驱动时只选择最关键的业务矩阵数据比如“不同用户角色的权限差异”“不同订单状态的展示差异”这类真的需要多组数据跑完整流程的场景。像用户名密码的格式校验这种交给接口层测就够了UI层只保留一条正常路径做冒烟测试即可。另外一个重要提醒UI层用数据驱动时sleep和显式等待的编写要格外小心。因为批量数据跑下来前一条用例可能在页面上遗留了弹窗或cookie状态会干扰后一条用例的正常执行所以一套用例跑完的清理逻辑比接口层重要得多。5.3 单元测试里的数据驱动单元测试的数据驱动比接口层更轻量通常直接写在代码里就够了。比如一个工具函数将金额转大写你只需要枚举几组输入输出做断言import pytest pytest.mark.parametrize(amount,expected, [ (0, 零元整), (1.5, 壹元伍角), (100, 壹佰元整), (-1, 金额必须大于0), ]) def test_amount_to_chinese(amount, expected): assert convert_amount(amount) expected这种场景直接用参数化装饰器一口气写完既没有文件读写的额外开销也没有数据维护的负担。单元测试的数据驱动原则和接口层是一样的但实现层面上能简化就简化不要为了用文件而用文件。写在后面的一些实际体会我自己从最开始手写用例到后来引入DDT数据驱动中间踩过不少坑但也实实在在体会到这个模式带来的改变。数据驱动最深刻的转变不是代码短了而是测试的思路变了你开始从一个“写测试脚本的人”变成“设计测试数据的人”。设计用例时你会更关注输入空间有哪些边界、哪些异常、哪些组合需要覆盖而不是困在某个页面上纠结元素要不要等三秒。这个视角一旦转换自动化用例的质量会肉眼可见地提高。给刚开始实践DDT的同学一个建议别一上来就搞数据库、搞测试平台、搞定时任务先用文件把数据驱动跑通把命名规范和断言习惯养成等到用例数量增多了、数据维护成为痛点时再考虑更高级的数据源和平台化方案。工具演进永远要跟着痛点走不要为了技术而技术。最后再分享一个小技巧不管你是用DDT库还是pytest参数化每写完一组数据驱动用例都花五分钟检查一下当你增加一条新数据时是不是真的不需要改动任何测试代码。如果还需要动代码说明数据还不够“彻底分离”再往下优化一层你就是真正掌握数据驱动测试了。
返回列表