ARTICLE DETAIL

资讯详情

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

接口自动化测试:Jmeter下数据初始化与脏数据清理实践

接口自动化测试:Jmeter下数据初始化与脏数据清理实践 做接口自动化测试的同行应该都遇到过这种糟心事用例第一次跑全绿隔几分钟再跑一次一批用例红了。一看日志什么唯一键冲突、数据已存在、订单号重复。这时候多半不是代码逻辑出了新Bug而是上一轮测试在数据库里留下的旧数据没有处理干净。我在用Jmeter做接口自动化测试的时候就经常被这种第二次执行才暴露的问题卡住。后面我总结出一条准则接口自动化跑得稳不稳一半看用例设计一半看数据初始化。这里说的初始化不是启动线程组那么简单而是指在测试执行前把测试环境里的旧数据清空、把数据库恢复到可控的基线状态。这篇文章我就围绕这个主题讲讲我用Jmeter清理旧数据、做数据初始化时积累的一套方法和踩过的坑给正在被脏数据折磨的测试同学一个可落地的参考。1. 为什么必须先清旧数据脏数据对接口测试结果的破坏路径1.1 最典型的翻车场景第二次执行就全红我先描述一个很常见的现象。你构造的用例是新增一个用户然后查询这个用户第一次跑的时候请求正常数据库里多了条用户记录断言通过。第二次再跑同一个用例又发了一次同样的新增请求结果接口直接返回该用户已存在或者虽然返回成功但列表接口的断言因为多了一条数据而失败。这在接口自动化里叫用例重复执行的不稳定性根因就是测试数据残留。如果你的测试脚本没有任何数据清理机制那每一次执行都会往环境里埋地雷地雷多了测试结果就开始随机飘红。更隐蔽的情况是你测试的是分页接口断言第一页返回10条第一轮没问题第二轮因为旧数据累积第一页变成了12条或者顺序被打乱断言就挂了。这种问题排查起来比数据已存在更费劲因为你第一眼根本看不出是测试数据的问题以为是接口逻辑变了。1.2 脏数据到底是怎么破坏断言结果的我把常见的数据残留危害整理了一下方便你对照排查破坏类型具体表现表面症状唯一约束冲突重复插入相同业务主键、唯一索引字段接口返回500或业务错误码幂等规则拦截接口本身有防重设计相同请求参数直接拒绝第二次执行直接失败统计类数据漂移列表总数、分页总数、聚合金额等断言被旧数据干扰断言值前后不一致关联数据错乱旧的外键记录影响新数据的级联操作删除失败、更新受影响行数不对状态字段残留旧的订单状态、用户状态影响流程类接口流转到下一步时判断异常核心原因是接口自动化测试天然具有可重复执行的要求但被测接口通常不具备执行前自动重置数据的能力。很多接口只是单纯地写库、读库它不会替你考虑这个测试用例要跑两遍。所以清空旧数据、初始化环境这个动作必须由测试脚本自己去完成。1.3 先判断你的项目是否需要清空式初始化不过我也要泼一盆冷水不是所有项目都适合上来就TRUNCATE清库。我自己接手的项目里就有两种情况是完全相反的处理方式需要清空式初始化测试环境数据量小用例强依赖从零开始的确定性。比如注册、下单、支付这类流程你希望每次跑都是从空库起步结果可预期。不适合清空式初始化测试环境里有大量基础配置数据如商品分类、区域列表、权限字典这些是接口逻辑的前置依赖清掉之后接口反而跑不起来。这种情况更适合局部清理——只清业务数据表保留配置表。所以你在设计初始化方案之前第一件事不是写SQL而是把数据库表分成两类基础配置表只读不清和业务数据表每次跑前清空。这是后面所有方案的前提。2. 初始化方案选型JDBC清库、API清理与脚本初始化怎么选想清楚了要不要清、清什么接下来就要选实现手段。我在Jmeter里实际用下来主流方案就三条路JDBC直连清库、调用业务清理API、通过JSR223脚本动态处理。三者的关系不是谁替代谁而是根据项目约束来选。2.1 JDBC直连清库最快但最需要谨慎Jmeter自带的JDBC请求JDBC Request Sampler可以直接连数据库执行SQL。优点是速度极快、不受接口逻辑限制一条TRUNCATE下去百万级表也瞬间清空。缺点也很明显你需要数据库账号权限而且绕过了业务逻辑万一删了不该删的配置数据后续用例全崩。如果项目允许测试人员直连测试库我的建议是选择JDBC方案作为主力。写法很简单配置一个JDBC Connection Configuration然后在JDBC Request里写SQL-- 清空订单明细表先清子表 TRUNCATE TABLE t_order_item; -- 清空订单表 TRUNCATE TABLE t_order; -- 清空用户表最后清父表 TRUNCATE TABLE t_user;但这里有一个很容易踩的坑TRUNCATE在MySQL里不能在有外键约束的情况下直接执行。如果你只是简单地把所有表TRUNCATE一遍大概率会报 Cannot truncate a table referenced in a foreign key constraint。我一会儿在第六部分专门讲这个坑的处理。2.2 走API业务清理安全但有前置条件有些测试环境出于安全考虑不开放数据库账号给测试人员。这种情况我就只能调后端提供的测试数据清理接口比如/test/clearUserData。这种方案本质是用业务手段删业务数据触发的是完整的事务逻辑外键、日志、缓存都能一并处理安全性最高。但它有一个很现实的问题清理接口不一定存在。很多项目的后端压根没有提供这类测试专用接口你只能去推动开发加。而且就算有清理接口本身也可能有Bug或者执行很慢把它当初始化手段等于是把一件本来很可靠的准备动作变成了一个新的不稳定因素。我的经验是如果开发愿意配合可以让他们做一个按业务范围清理的接口而不是全表TRUNCATE。比如POST /api/test/clean?domainorder只清订单域的数据保留用户和商品配置。这个接口后期还能复用到生产环境的数据订正场景投入产出比很高。2.3 脚本化初始化方案复杂度高时的兜底选项第三条路是JSR223 SamplerGroovy里嵌脚本动态控制清理逻辑。最典型的场景是你不仅要清旧数据还要顺便构造一批新数据或者清理逻辑牵扯到多个服务、需要先查再删。这个方案的优点是灵活可以在一个脚本里做查询-判断-清理-重建完整流程缺点是脚本维护成本高而且Groovy脚本在Jmeter里的调试体验不如直接用JDBC直观。我一般只在JDBC方案搞不定的情况下才用它。比如我遇到过一张表没有外键但应用代码里做了跨表逻辑校验直接删数据会导致缓存不一致。解决办法就是在Groovy脚本里先调用一个内部接口刷新缓存再删除数据import java.sql.* def conn vars.getObject(initConn) def stmt conn.createStatement() // 先清理缓存 def resp new URL(http://test-server:8080/internal/cache/refresh).getText() log.info(Cache refresh result: resp) // 再清数据 stmt.execute(DELETE FROM t_order WHERE create_time NOW() - INTERVAL 1 DAY)这其实就是一种半业务半数据的清理方式用得好能解决很多奇怪的数据问题。3. 实战搭建一套JDBC数据初始化流程可复用模板聊完选型我直接给你一套我目前在项目里用了大半年的JDBC初始化流程。它不花哨但稳定基本思路就三步连库、按依赖顺序清表、重置自增序列。3.1 前置准备驱动、连接配置与权限最小化在Jmeter里用JDBC之前先把对应数据库的驱动JAR包放好。以MySQL为例把mysql-connector-java-xxx.jar放到Jmeter的lib目录下重启Jmeter。然后在测试计划里加一个JDBC Connection Configuration配置里最需要注意的是Database URL写清楚连接串比如jdbc:mysql://10.0.1.100:3306/test_db?useUnicodetruecharacterEncodingutf8JDBC Driver Classcom.mysql.cj.jdbc.Driver新版驱动包Username / Password我建议单独建一个测试专用账号只给这个账号DELETE、TRUNCATE、SELECT权限别用root。万一脚本写错权限越小爆炸半径越小。Variable Name这个要记牢后面JDBC Request全靠这个变量名找到连接池。我习惯命名为initConn。连接池的Max Wait建议设置 10 秒以内因为初始化阶段如果数据库响应慢不应该拖到后面用例超时才暴露早失败早处理。3.2 一个真实案例用户-订单-明细三级表结构的清理脚本为了让流程更具体我用一个经典的三级结构举例用户表t_user、订单表t_order、订单明细表t_order_item。三者存在外键关系订单明细属于订单订单属于用户。在这种结构下清空顺序必须是先子表后父表否则删除父表数据时会受外键约束拦截-- 第一步清空订单明细子表 TRUNCATE TABLE t_order_item; -- 第二步清空订单中间表 TRUNCATE TABLE t_order; -- 第三步清空用户父表 TRUNCATE TABLE t_user;把这三条SQL分别放在三个JDBC Request里或者直接在一条SQL里用分号分隔但要注意JDBC Request默认Query Type选Callable Statement或Statement时部分驱动支持多条语句安全起见我推荐每个表单独一个JDBC Request理由有二单条出错时日志定位清楚可以在每个请求后面单独加断言。光清了表还不够自增ID也是个隐患。TRUNCATE在MySQL里会自动重置自增但DELETE不会。如果你用的是DELETE方式后续插入的ID不会从1开始有些用例断言里如果硬编码了ID就会踩雷。所以清完表之后有必要的表再执行一次ALTER TABLE t_user AUTO_INCREMENT 1; ALTER TABLE t_order AUTO_INCREMENT 1; ALTER TABLE t_order_item AUTO_INCREMENT 1;3.3 多环境复用的参数化配置真正的工程化项目不会只有一个测试环境可能是dev、sit、uat好几套。如果初始化脚本里把数据库IP、账号写死换环境就要改脚本维护成本不可接受。我的做法是用Jmeter的测试计划属性Test Plan Properties结合环境变量来做多环境配置。具体操作是初始化连接配置里的Database URL写成jdbc:mysql://${__P(db_host)}:${__P(db_port)}/${__P(db_name)}?useUnicodetruecharacterEncodingutf8在命令行执行时通过-J参数动态注入jmeter -n -t init_clean.jmx -Jdb_host10.0.1.100 -Jdb_port3306 -Jdb_nametest_db -l result.log这样同一份初始化脚本在三个环境都能跑不需要打开Jmeter改配置。如果你是在GUI里调试可以在用户自定义变量里写一份默认值命令行执行时再用-J覆盖两头兼顾。另外我强烈建议给初始化脚本单独保存成一个.jmx文件和正式用例脚本分开。原因后面讲执行顺序编排时你会更明白。4. 动态数据初始化主键冲突、自增序列与关联表的连锁处理4.1 清表之外自增列与序列的复位很多人以为清空旧数据就是把所有表DELETE/TRUNCATE一遍但实际做接口自动化时光清表还不够自增主键的复位经常被忽略。虽然TRUNCATE会自动复位但遇到下面两种情况就必须手动处理你用的是DELETE FROM t_user而不是TRUNCATE例如因为外键限制不能TRUNCATE只能DELETE数据库是PostgreSQL或Oracle自增由独立的SEQUENCE管理PostgreSQL下重置序列的SQL长这样TRUNCATE TABLE t_user RESTART IDENTITY CASCADE;这一条语句同时完成了清表和序列复位比分开写更省事。如果你担心误用CASCADE把关联表也清了可以先手动把关联关系理清楚再决定。Oracle则要单独重置序列ALTER SEQUENCE seq_user_id RESTART START WITH 1;这里我想表达一个观点初始化不是简单地删数据而是要把数据库的整体状态回退到一个已知的、可控的基线。自增ID就是基线的一部分。如果你不清序列跑出来的ID是乱七八糟的一旦用例里有根据ID查询这种断言后续维护成本会直线上升。4.2 受外键约束的删除顺序先子表后父表这部分我在实战里遇到了太多次值得单独拎出来讲。前面提到过TRUNCATE的外键限制这里我展开讲一下细节。MySQL里如果表A被表B的外键引用直接TRUNCATE表A会报错。所以处理外键约束的常见姿势有两种第一种严格按依赖顺序删除子表先删、父表后删。这种适合表结构清晰、层级不深的场景。第二种在同一个会话里先禁用外键检查再执行所有TRUNCATE最后恢复SET FOREIGN_KEY_CHECKS 0; TRUNCATE TABLE t_user; TRUNCATE TABLE t_order; TRUNCATE TABLE t_order_item; SET FOREIGN_KEY_CHECKS 1;这里有一个非常关键的点Jmeter的JDBC Request默认每个请求单独一个事务。假设你在一个请求里执行上面四条SQL那没问题但如果你把它们拆成四个JDBC RequestSET FOREIGN_KEY_CHECKS 0只在一个会话里有效下一个请求新开的连接是不会继承这个设置的。解决方案有两个一是把所有初始化SQL合并到一个JDBC Request里执行用Statement类型支持多语句二是在JDBC Connection Configuration里把Transaction Isolation设置好并确保所有请求使用的连接来自同一个连接池且保持Auto Commit关闭这样会话状态能延续。我在实际项目里用的是方案一简单粗暴且不容易出错。4.3 从清空旧数据到重建数据基线一部分接口自动化用例不仅仅需要空库还需要在空库基础上准备特定状态的数据。比如测试已支付订单的退款接口你光清空旧数据没用你还得先造一个已支付的订单退款接口才有东西可退。所以我把初始化分成两段第一段清空旧数据前面说的所有清理操作第二段造数插入测试前置数据这两段建议放在同一个线程组里顺序执行。造数可以用JDBC Request直接INSERT也可以调用业务接口造数。我的判断标准是如果插入的数据只是存在即可用SQL最快如果插入的数据还要走业务规则校验就调接口。前者适合字典数据、配置数据后者适合订单、用户这类带状态机的数据。举一个实际场景。我在测试一个优惠券发放接口时前置条件是必须有三个已上架的商品且库存大于10。我用JDBC直接插商品表是不行的因为商品状态字段背后有一套校验逻辑直接INSERT进去的状态可能不合法。所以这个场景我选择先调创建商品接口造3个上架商品再跑优惠券发放用例。记住了SQL只能保证数据存在业务接口才能保证数据合法。5. 执行顺序编排把初始化放进自动化测试的执行流5.1 setUp线程组官方给我们的初始化入口如果你把初始化脚本和正式用例脚本放在同一个jmx文件里最简单的做法是利用Jmeter的setUp线程组。它的特性是会在普通线程组执行前先运行完并且只有setUp线程组全部执行且没有任何失败普通线程组才会开始。我建议把清空旧数据重建基线的所有步骤都放在setUp线程组中这样每次跑测试初始化自动执行不用单独记着先跑清理脚本初始化失败时普通线程组不会执行避免带着错误基线跑一堆无效用例测试结果文件里能清楚看到初始化阶段的耗时和错误实际使用时我会在setUp线程组的最后一个采样器后面加一个JSR223断言校验关键表的数据量是否归零或达到预期。比如def conn vars.getObject(initConn) def stmt conn.createStatement() def rs stmt.executeQuery(SELECT COUNT(*) AS cnt FROM t_user) if (rs.next() rs.getLong(cnt) ! 0) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(用户表清空失败剩余记录数: rs.getLong(cnt)) }一开始可能觉得多余但用过就回不去了。它能帮你把环境没准备好和被测接口有Bug这两类问题在时间线上区分开。5.2 要不要单独跑一份初始化job如果你用的是持续集成CI来触发接口自动化我建议不要依赖测试脚本内的setUp而是把初始化作为一个独立的job在测试job之前执行。这样做的原因有两点第一独立job可以随时手动触发。联调的时候你想把环境重置一下不需要打开Jmeter跑整个测试计划单独跑初始化job就够了。第二初始化job可以接入定时调度。比如每天早上8点执行一次清库8点半再跑自动化测试这样即使自动化脚本漏了清理逻辑环境也至少是当天干净的。执行方式也很简单用命令行模式跑一个单独的初始化jmx文件jmeter -n -t clean_and_init.jmx -Jenvsit -l init_result.jtl需要注意CI上跑初始化的时候要确保这个job只针对测试环境。我之前见过同事把线上的数据库连接配置写到init脚本里差点把线上表清了。现在我的做法是init脚本里对数据库名做一个白名单校验数据库名不是test、sit、uat这些明确标识的直接断言失败停止执行。安全开关永远不嫌多。5.3 初始化失败的快速感知与中断机制还有一个细节容易被忽视初始化失败后后续用例该不该继续跑我的答案是不要继续跑。带着一份没清理干净的数据跑用例得到的结果既不能证明接口是对的也不能证明接口是错的纯粹浪费时间和机器资源。在Jmeter里实现初始化失败即中断的办法很简单。在setUp线程组里给每个初始化请求加一个断言比如响应数据包含成功标识或者影响行数大于等于0。然后在线程组上勾选Treat Sampler Errors as a Failure并设置Start Next Thread Loop的停止策略。更稳妥的做法是把Action to be taken after a Sampler error设置为Stop Test这样一旦初始化失败整个测试立即终止。我之前就吃过一次亏初始化SQL写错了setUp线程组里报了错但测试计划没有配置停止结果普通线程组照样跑150个请求全部基于脏数据执行最后的测试报告一片红排查了半小时才发现是初始化没生效。从那以后我对所有含初始化步骤的测试计划一律配置Stop Test。6. 我在实践中踩过的坑初始化反而把环境搞坏的情况6.1 连错库的灾难初始化脚本的安全开关前面提过连错库的风险这里展开讲一个真实故事。有一回我在本机调试初始化脚本本地和测试环境的数据库连接串长得非常像只是IP最后一段不一样。我改完脚本没仔细核对连接串直接执行结果瞬间把本地开发库的表全清了。当时我还在想这库怎么这么快就清完了过了好一会儿才反应过来好在本地库没有重要数据不然就是严重事故。从那之后我做初始化脚本一定会做三件事在JDBC Request里第一条SQL不是清表而是先查当前连接的库名做一个断言校验数据库账号使用最小权限的专用账号不开TRUNCATE权限以外的任何高危权限比如DROP DATABASE根本禁止在连接串里明确拼接allowMultiQueriesfalse防止一条语句意外触发多条SQL这些看起来都是防呆操作但对这种有破坏性的脚本来说多一道保险就少一次事故。6.2 外键与触发器关闭外键检查的正确姿势前面说到了SET FOREIGN_KEY_CHECKS 0的会话问题。除了外键触发器和存储过程也是一类隐患。有些业务表上有INSERT/UPDATE触发器如果你用DELETE清数据触发器可能也会跟着执行导致一些你意想不到的脏数据写到关联表。我踩过的一个坑是这样清理订单表的时候DELETE语句触发了订单日志表的INSERT触发器结果订单表清空了订单日志表反而多了一大堆订单删除日志。后续测试日志查询接口时断言数据条数怎么都不对。所以我在设计初始化流程时会专门检查目标表上有没有业务触发器如果有就需要在初始化事务里先禁用再清理-- MySQL 8.0 之前的写法 SET OLD_SQL_MODE SQL_MODE; SET SQL_MODE ; DELETE FROM t_order; SET SQL_MODE OLD_SQL_MODE;或者更稳妥的办法如果这个表不需要保留任何数据直接用TRUNCATE因为它不会触发行级DELETE触发器。但注意TRUNCATE本身在MySQL里会隐式提交无法回滚所以执行前一定要确认连接的是正确的库。6.3 恢复现场清完之后的快速验证最后分享一个习惯每次初始化执行完之后我会用一个快速验证请求来确认环境真的处于可用状态。不是简单的查表数量而是调用一个真实业务接口做冒烟判定。比如初始化完成后紧接着请求一次查询商品列表接口断言返回200且列表为空。这个步骤能模拟真实用例的执行场景又能在早期暴露清库把配置表也清了这种低级错误比单纯查数据库更贴近用户视角。这个验证步骤我通常就放在setUp线程组的末尾耗时不会超过1秒但是给整个自动化测试吃下了一颗定心丸。回到开头说的那个场景——第二次执行全红的问题。如果你现在还在被旧数据困扰我的建议是不要继续在用例层面打补丁了。与其每个用例都加如果数据存在就先删除的逻辑不如花两个小时把初始化流程单独做出来做成一个标准的setUp阶段。等初始化稳定之后你再去回头看那些曾经偶尔全红的测试报告会发现90%的随机失败都已经消失了。清空旧数据看起来是个不起眼的活儿但它在接口自动化里的地位和地基一样——看不见但决定了整栋楼能盖多高。
返回列表