多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JMeter数据驱动测试:CSV Data Set Config深度拆解与避坑指南

JMeter数据驱动测试:CSV Data Set Config深度拆解与避坑指南 聊到JMeter里的数据驱动测试有一半的坑都出在 CSV Data Set Config 这个元件上。它看起来很简单填一个文件路径写几个变量名后面请求里用${}引用就行了。可实际用起来乱码、数据重复、读不到文件、所有线程拿同一行、循环几轮之后变量变成空——这些问题我在不同项目里反复见到自己也踩过不少。这篇内容会把 CSV Data Set Config 从参数细节、执行机制、作用域到多线程取数模型、断言联动、分布式执行完整拆一遍。适合已经能跑通基础 JMeter 脚本、但想真正把数据驱动测试做稳做准的人。先讲原理再给可以直接复制的实操配置最后是高频问题速查。1. CSV Data Set Config 的核心机制先搞懂它到底怎么发数据很多人用不好这个组件不是因为不知道界面上的字段怎么填而是没理解它的执行时机和取数模型。CSV Data Set Config 不是启动时一次性把所有数据读进内存而是在每个取样器执行前按需读取一行读到的值放进当前线程的变量空间。这个机制决定了它放在哪里、怎么循环、怎么共享都会直接影响脚本行为。1.1 执行时机与作用域为什么“放在哪里”比“填什么”更关键把 CSV Data Set Config 想象成一个发牌器每次请求之前它从牌堆里摸一张牌发到变量里。牌堆是所有人共用还是每人一副取决于 Sharing mode摸完一轮之后是重新洗牌还是直接结束取决于 Recycle on EOF 和 Stop thread on EOF。具体到执行顺序配置元件会在同一作用域下的取样器执行前生效。也就是说你把 CSV Data Set Config 放在线程组下那么该线程组里的每个请求在每次执行前都会尝试从文件里读一行。如果你把它放在某个 HTTP 请求下那只有这个请求会去读数据。如果你把它放进“仅一次控制器”那基本只会读取一次后续循环不会再更新变量——这是很多脚本“变量永远停留在第一个值”的常见原因。作用域匹配是个很容易忽略的细节。举个例子一个线程组下有 A、B 两个 HTTP 请求CSV 配置放在 A 请求下那么 A 每次执行会读新行B 不会。如果 B 里用了${username}它只会拿到上一次 A 请求产生的值或者首次执行时直接为空。所以我的习惯是只要数据驱动贯穿整个业务流程就把 CSV Data Set Config 放在线程组层级而不是某个单独的取样器下面。1.2 九个参数逐个拆默认值背后全是坑CSV Data Set Config 界面上的字段不多但每个都有说法。我按实际使用频率把关键参数整理成一张速查表配置项推荐值/可选值容易踩的坑Filename绝对路径或data/xxx.csv分布式压测时相对路径基于 JMeter 启动目录Agent 机器上可能不存在该路径File encoding建议填UTF-8留空时用系统默认编码Windows 中文环境默认 GBK中文数据容易乱码Variable Names用英文逗号分隔如username,password如果不填JMeter 会自动把 CSV 第一行当变量名如果填了第一行会被当数据处理Delimiter默认逗号可按需改成\t或 Allow quoted data默认 False字段被双引号包裹且内容含逗号时不开启就会错误拆列Recycle on EOF默认 True循环压测时不勾文件读完变量不再更新勾上后数据会从头重复Stop thread on EOF默认 False如果想让每个线程读完一行数据就停止需要勾选Sharing mode默认 All threads选错模式会导致所有线程拿同一行或者数据分配不符合预期单独说一下 Recycle on EOF 和 Stop thread on EOF 的组合逻辑。实践中如果你两个都勾上Stop thread on EOF 实际上不会触发因为 Recycle 为 True 时读到文件末尾会重新打开文件继续取数根本不会走到“停止线程”的逻辑。反过来只勾 Stop thread on EOF 不勾 Recycle当文件读完后线程会在下一次需要取数时直接停止适合“一条数据对应一次请求”的回归场景避免重复使用同一批数据。Sharing mode 是另一个重灾区。默认的 All threads 表示所有线程共享同一个取数游标一个线程取走一行另一个线程不会重复取同一行适合多线程分配不同数据。Current thread group 则是每个线程组单独维护一份游标组内线程共享不同线程组之间互不影响。Current thread 是每个线程各自从头读文件每个线程都会拿到完整的数据集——如果你只是想给 100 个线程分配 100 行数据千万不能用这个模式否则每个线程读到的都是第一行。1.3 变量引用边界${username}、vars.get 和跨线程组传递CSV Data Set Config 定义的变量在 HTTP 请求参数、请求体、响应断言里都可以直接用${username}引用。但在 BeanShell 或 JSR223 脚本里我不建议直接用${username}因为脚本引擎在编译阶段就可能把${}替换成当时的字符串字面量后续循环中变量更新了脚本里的值却不会变。更稳妥的写法是通过vars.get(username)动态获取保证每次执行都读到当前值。跨线程组的情况要特别注意。CSV 配置产生的变量只属于当前线程其他线程组默认拿不到。如果你非要在不同线程组之间共享数据得用props.put()或__setProperty()把值提升到全局属性再在目标线程组用__P()读取。能绕开就绕开这种跨线程传值会让脚本的可读性和稳定性都下降。2. 数据驱动场景拆解什么样的数据适合放进CSVCSV Data Set Config 不是所有参数化场景的最优解。做数据驱动之前先想清楚需求是给 100 个用户分配不同账号还是给 1000 次请求生成动态时间戳前者适合 CSV后者适合函数或脚本生成。数据也有静态和动态之分静态的主数据、账号、配置项可以放文件动态的 token、时间戳、随机数最好用代码生成。2.1 多账号登录与Token链式传递最常见的场景是压测登录接口或者用多个真实账号跑业务流程。CSV 里存账号和密码每个线程从文件里读一行登录后拿到 token后续请求带着 token 访问业务接口。这里的关键点不在 CSV 本身而在 token 的传递。登录接口返回的 token 需要用 JSON 提取器或正则提取器保存到变量后续接口通过 HTTP 头管理器引用。CSV 只负责提供账号密码token 是动态生成的两者不要混在一起。实际脚本里我一般会这样设计线程组 ├─ CSV Data Set Config # 读取账号密码 ├─ HTTP请求 - 登录 # 返回 token ├─ JSON提取器 - 提取 token # $.data.token ├─ HTTP头管理器 - Authorization # Bearer ${token} ├─ HTTP请求 - 查询订单 # 业务请求 └─ 断言组件这个结构里CSV 是数据源头JSON 提取器负责动态关联。如果登录接口响应里的 token 字段是嵌套结构JSON Path 就要写准比如$.data.token拿不到时默认值一定要填否则后续请求会拿到一个不存在的变量。2.2 请求体模板化把业务字段做成数据列遇到创建订单、提交表单这类接口很多人喜欢在请求体里写死数据然后通过 CSV 参数化每个字段。这种做法的核心是构造好请求体模板。比如下单接口CSV 文件里放三列orderId,productId,amount请求体写成{ orderId: ${orderId}, productId: ${productId}, amount: ${amount} }注意amount是数字类型不能加引号orderId和productId是字符串必须带引号。这种细节写错接口返回类型错误排查起来特别浪费时间。更复杂的情况是请求体里嵌套数组比如商品列表有多条可以拆成多行 CSV配合循环控制器多次请求或者用 Groovy 在 JSR223 前置处理器里动态拼 JSON。2.3 用CSV驱动断言期望结果也要参数化数据驱动不只是把请求参数换成变量断言也可以跟着数据走。CSV 文件里加一列期望的返回码或提示信息断言组件对比实际响应这样一组测试数据就同时覆盖了正向和反向用例。响应断言可以直接引用变量比如检查响应里是否包含${expectMsg}。但如果要做更复杂的判断比如从响应里提取某个字段再和 CSV 里的期望值比较建议用 JSR223 断言。下面这个 Groovy 示例就是我从实际项目里简化出来的def resp prev.getResponseDataAsString() def expect vars.get(expectMsg) if (!resp.contains(expect)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(期望响应包含 expect 实际响应 resp) }这里的prev是上一次取样结果vars是当前线程的变量容器AssertionResult是断言结果对象。用这种方式断言逻辑完全捏在自己手里比配置化的响应断言灵活得多。2.4 文件上传接口的参数化路径、MIME一起上上传文件的接口同样可以数据驱动。CSV 文件里放文件路径、参数名、MIME 类型HTTP 请求的 Files Upload 里全部引用变量filePath,mimeType,paramName /data/images/a.jpg,image/jpeg,file /data/images/b.png,image/png,file文件路径要用每一台执行机器上都存在的路径。本地调试没问题分布式压测时如果 Agent 上没有这些文件请求会直接失败。更稳妥的做法是把测试文件随脚本一起分发路径保持同步或者用相对路径配合统一目录结构。3. 实操一个“登录查询订单”的数据驱动脚本全流程光讲理论容易飘下面完整走一遍。假设有一个登录接口和一个查询订单接口登录返回 token查询订单需要带 token。我要用 5 个测试账号每个账号跑 2 轮验证登录和查询流程是否正常。3.1 准备数据文件格式错了后面全白搭先准备一个accounts.csv第一行是变量名后面是数据行。这里刻意加了expectCode和expectMsg两列一会断言要用username,password,expectCode,expectMsg tester01,Passw0rd1,0,登录成功 tester02,Passw0rd2,1,用户名或密码错误文件保存时选 UTF-8 编码不要带 BOM。放到一个固定目录比如D:/jmeter-data/accounts.csv。文件末尾不要留空行否则多线程读到最后可能拿到一行空数据变量变成空字符串后面断言直接失败。如果你用 Excel 编辑过 CSV导出时特别容易带入不可见字符比如 BOM 或者\r。经验是保存后用文本编辑器打开看一眼确保第一行没有\ufeff之类的前缀。3.2 测试计划骨架与线程模型设计测试计划结构如下测试计划 └─ 线程组线程数5循环次数2 ├─ CSV Data Set Config ├─ HTTP请求 - 登录 ├─ JSON提取器 - 提取token ├─ HTTP头管理器Authorization: Bearer ${token} ├─ HTTP请求 - 查询订单 └─ JSR223断言线程数 5、循环次数 2一共会执行 10 次登录请求。CSV 数据行也是 5 行在 All threads 模式下第一次循环线程 1 到 5 分别读到第 1 到第 5 行第二次循环时如果 Recycle on EOF 勾上了会从头继续读第 1 到第 5 行所以测试数据会被重复使用一次。这里要提前算一笔账总取数次数 线程数 × 循环次数。如果这个乘积大于 CSV 总行数要么允许数据重复勾 Recycle要么就会有一部分线程取不到数据。压测前先把这个乘法和文件行数对比一下能避免很多莫名其妙的“数据不够用”问题。3.3 CSV Data Set Config 推荐配置这个场景下我的配置如下配置项填写内容/选项说明FilenameD:/jmeter-data/accounts.csv本地调试用绝对路径File encodingUTF-8防止中文乱码Variable Namesusername,password,expectCode,expectMsg和文件列一一对应Delimiter,字段简单逗号够用Allow quoted dataFalse数据里没有逗号和引号Recycle on EOFTrue循环次数为 2需要重新读取Stop thread on EOFFalse不停止线程让它循环Sharing modeAll threads5 个线程分配 5 行数据注意文件名用绝对路径是因为 JMeter 的相对路径基于启动目录默认是 JMeter 的 bin 目录。如果文件放在别处相对路径就会找不到。等脚本稳定之后要上分布式压测再改成通过命令行属性传路径后面第 5 节会讲。3.4 后置处理器与断言怎么写登录请求添加 JSON 提取器变量名填tokenJSON Path 表达式按接口实际返回写。如果返回体是{code:0,data:{token:abc123}}表达式就是$.data.token。关键一项Default Value 一定要填比如TOKEN_NOT_FOUND。这样即使提取失败后续请求会明确暴露问题而不是卡在一个不存在的变量上。查询订单请求添加 JSR223 断言用 Groovy 写def resp prev.getResponseDataAsString() def expectCode vars.get(expectCode) if (!resp.contains(\code\: expectCode)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage(期望code expectCode 实际响应 resp) }这里用的是 JSR223 Groovy而不是 BeanShell。JMeter 官方也推荐 Groovy性能和语法都比 BeanShell 好。老项目里常见的 BeanShell 脚本逻辑也差不多但新脚本就别再用 BeanShell 了。3.5 压测前的取数验证脚本跑起来之前先别急着上高并发。我的习惯是临时加一个 Debug Sampler放在线程组最前面循环次数设小一点比如 1 次然后跑一遍查看结果树里 JMeter Variables 面板确认每个线程读到的username是否分散、expectMsg是否正确。确认没问题之后把这个 Debug Sampler 删掉。Debug Sampler 会把当前线程的所有变量打到结果树里压测时留着一个是不必要的性能损耗二来结果树里会刷出一大堆变量信息影响查看业务响应。另一个验证方法是临时加一个 JSR223 后置处理器把线程名和当前变量写进日志log.info(thread Thread.currentThread().getName() , username vars.get(username) , expectMsg vars.get(expectMsg))跑完后去 jmeter.log 里搜thread能清楚看到每个线程分配到哪一行数据。这个方法在排查“所有线程拿同一行”问题的时候特别好用。4. 高频问题速查最常踩的坑和我的排查思路这部分我按问题现象整理附上原因和对策都是实际项目里反复出现过的。4.1 第一行数据被吃掉或变量名错乱CSV Data Set Config 的 Variable Names 字段如果留空JMeter 会自动把 CSV 第一行当作变量名并且跳过这一行不当作数据。如果你填了 Variable Names第一行就会被当成真实数据处理。所以最稳妥的方案是二选一要么文件里不写表头Variable Names 里手动填变量名要么文件里保留表头Variable Names 留空让 JMeter 自动识别。最怕的就是文件有表头、Variable Names 里又填了变量名结果第一行测试数据被当成表头跳过了。4.2 所有线程拿到的都是同一行数据这个问题十有八九是 Sharing mode 用错了。如果选成 Current thread每个线程都从头开始读文件所有线程都会拿到第一行。改成 All threads 或 Current thread group 之后多线程就会分散取数。另一种可能是 CSV 配置放在了“仅一次控制器”或某个取样器下面导致只有第一次执行读了数据后续请求拿到的是上一次缓存的变量值。检查一下配置元件在测试计划树里的位置放到线程组层级通常能解决。4.3 本地能跑远程不行分布式压测时CSV 文件必须在每一台 Agent 机器上都存在而且要保证路径一致。如果你本地用的是相对路径比如data/accounts.csv它是相对于 JMeter 启动目录去解析的每台 Agent 的启动目录未必相同Agent 上也不一定有这个文件。我的做法是把脚本里写死路径改成属性引用执行时通过-J参数动态传入jmeter -n -t test.jmx -JcsvFile/data/accounts.csvCSV 配置里的 Filename 填${__P(csvFile,data/accounts.csv)}这样本地不传参时走默认路径分布式时每台机器可以传各自的路径。文件要提前同步到所有执行节点路径一致基本不会再出问题。4.4 中文乱码中文乱码通常是文件编码和 JMeter 解码不一致导致的。CSV 文件本身是 GBKFile encoding 留空走系统默认编码Windows 中文系统默认就是 GBK看起来可能正常一旦换到 Linux 执行机或者文件变成了 UTF-8乱码就会出现。统一方案文件保存为 UTF-8 无 BOMFile encoding 填UTF-8。如果响应里的中文还是乱码检查 HTTP 请求的 Content Encoding 是不是UTF-8也可以在 JSR223 后置处理器里强制设置响应编码。4.5 字段里含逗号或换行CSV 字段本身如果包含逗号比如地址列北京市,朝阳区用默认逗号分隔就会被拆成两列。解决办法有两个一是把分隔符改成\t或|前提是字段里没有这些字符二是字段用双引号包起来并且勾上 Allow quoted data。Excel 导出的 CSV 会自动给含逗号的字段加引号所以如果你直接用 Excel 编辑数据记得检查 JMeter 里有没有开启 Allow quoted data。还有一个隐藏雷字段里如果包含换行勾了 Allow quoted data 也能解析但很多文本编辑器显示正常导入 JMeter 后却错位保险做法是尽量避免多行字段。4.6 唯一性冲突用手机号、用户名这类有唯一约束的字段做压测第一轮跑完数据落库第二轮再跑就会因为重复数据报错。最简单的思路是构造可重复使用或不落库的数据如果业务必须使用真实唯一值就把时间戳或随机数拼接进去。CSV 里存固定前缀请求参数里再拼动态部分{ mobile: 138${__time(yyyyMMddHHmmss)} }但如果你的数据模型依赖 CSV 里的完整账号那只能在每轮压测前重新生成一次数据文件或者清理测试数据。4.7 多线程组共享同一个 CSV 文件多个线程组读同一个文件All threads 模式下是全局共享游标两个线程组可能交替取数导致第二线程组拿到的行和第一组完全不连续。如果两个线程组需要各自独立读取完整数据集就把 Sharing mode 改成 Current thread group每个线程组各读各的组内线程共享游标。还有一个小提醒多个 CSV Data Set Config 读取同一个文件时即使配置看起来一样游标也是各自独立的。不要指望两个 CSV 配置读取同一个文件时行号能对得上除非你刻意控制。5. 进阶玩法CSV之外的数据驱动选型CSV Data Set Config 是一个非常好的起点但真正复杂的场景里它并不是唯一选择。这里分享几个我常用的进阶打法可以根据项目情况组合使用。5.1 动态参数用代码生成静态数据才放CSVCSV 适合存放“预先准备好、不会变”的数据比如账号、商品 ID、配置项。像时间戳、随机数、唯一序列这类每次执行都要变的值就别放进 CSV 了。用函数或 JSR223 生成既省去准备文件的时间也不会出现数据用尽的问题。def orderId ORD System.currentTimeMillis() def userId vars.get(username) _ (1..100).collect{ (char)(a it % 26) }.join() vars.put(dynamicOrderId, orderId) vars.put(dynamicUserId, userId)多线程并发时时间戳重复的概率很低但极端情况下仍可能撞建议加上线程编号或随机数再拼一层。这种动态参数和 CSV 里的静态数据配合既能保证数据可读性又能满足唯一性要求。5.2 数据库实时取数与CSV离线数据的配合如果测试数据必须从业务库里实时获取CSV 就不合适了。用 JDBC Connection Configuration 连接数据库JDBC Request 查询结果再通过 ForEach 控制器遍历结果集把每一行数据作为请求参数。这种方式适合数据量大、且需要和线上库保持一致同步的场景。缺点是脚本依赖数据库连接执行环境需要开通网络权限。我的习惯是离线可准备的数据用 CSV动态实时数据用 JDBC。两者结合时CSV 里放账号和业务主键JDBC 里查辅助字段各取所长。5.3 多个CSV配置并联时的对应关系一个请求可能需要多个数据源比如账号来自accounts.csv订单数据来自orders.csv。你可以在测试计划里放两个 CSV Data Set Config分别定义变量前缀比如acc_username、acc_password、ord_id、ord_amount。要注意两个 CSV 配置的取数游标是独立的你不能假设第 1 行的账号一定对应第 1 行的订单。如果业务上要求两者按行对齐最好的办法是把两张表合并成一个 CSV 文件所有字段放在同一行里。分文件的方案只适合两者独立关联的场景。5.4 命令行属性传路径分布式压测的救星前面提过用${__P()}读取属性这里再展开说。CSV 配置里的 Filename 不写死而是写成${__P(csvFile,data/accounts.csv)}执行压测时本地可以不加参数直接用默认路径分布式压测时每台 Agent 或执行命令里传入各自机器上的绝对路径jmeter -n -t test.jmx -JcsvFile/data/accounts.csv这个写法解决了一个实际问题不同执行机的目录结构可能不一样各自指定路径后就不再需要强制所有机器保持一致的目录结构。5.5 把运行结果写回文件提取值归档与失败日志有些场景需要把测试过程中提取到的值导出比如登录 token、生成的订单号。JSR223 后置处理器可以直接追加写入本地文件def line vars.get(orderId) , vars.get(token) \n new File(/data/output.txt).append(line)注意多线程并发写同一个文件时可能会互相覆盖或抛异常。稳妥的做法是给每个线程生成独立的文件或者用线程号做文件名后缀最后再合并。另一种场景是把断言失败的请求原文、响应内容写入失败日志文件方便压测结束后统一分析比只看聚合报告直观得多。6. 压轴经验数据准备比脚本配置更重要写到这里分享一个我自己最大的体会。CSV Data Set Config 的参数再多、坑再多其实都属于“会用就能避开”的问题。真正让数据驱动测试翻车的往往是数据准备阶段文件行数不够、数据没有唯一性、账号在生产环境被锁定、压测前忘了清理上一轮产生的脏数据。现在我在每个压测项目里动手写脚本前一定会先列一个简单的关系式数据行数 vs 线程数 × 循环次数 数据是否允许重复 数据是否需要跨线程、跨线程组共享 数据文件是否需要在多台执行机同步把这四个问题想清楚再回来看 CSV Data Set Config 的配置基本上不会出大的偏差。脚本配置只是手段数据模型才是数据驱动测试的核心。下次再遇到“脚本没问题但结果不可信”的情况先别急着调线程数回头看看数据文件问题大概率就在那里。
返回列表