ARTICLE DETAIL

资讯详情

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

Shell批量执行测试用例与自动生成报告实战指南

Shell批量执行测试用例与自动生成报告实战指南 测试这个职业干久了会形成一个共识最耗人的工作往往是把测试用例跑完、把结果收齐、再写出测试报告这套重复劳动——直到你学会用Shell做批量执行才算是把这套流程真正解放出来。这个系列写到第88章我想聊一个最朴素但收益极高的技巧用Shell把测试用例批量执行起来顺手自动生成测试报告。Shell写起来不性感但它恰好长在测试工程的“缝”里——接口用例可以调、UI用例可以调、移动端adb命令也可以调你只要把一个一个用例变成有确定退出码的脚本剩下的调度和汇总机器都能替你干。适合谁手里管着一堆自动化脚本却还靠手工点来点去的人以及刚转自动化测试、想快速搭一套可用流程的同学。1. 为什么测试工程师需要Shell这手批量活1.1 手工回归是时间黑洞还是流程遮羞布先说实话手工回归不是不能做它适合探索式测试、适合需求还没稳的阶段。但如果你的项目已经有了一批接口测试脚本、UI脚本甚至只是几十条curl命令还坚持一条条复制进终端执行那问题不在工具在流程。我见过最夸张的情况一个回归包里有120条功能用例其中80条是脚本化的执行人还是开着终端一条条贴命令贴完手动记结果最后拿Excel汇总。这不是慢一点的问题是根本不可持续——脚本越多人力反而越重。真正做过几次版本回归的人都能感受到人肉执行最大的问题不是慢而是不稳定。你今天点了20条用例没记住哪条失败明天忘了刷新环境后天看漏一个报错结果就是报告全绿、上线出事故。所以批量执行并不是“省时间”这么简单它是在把执行过程变成可重复、可追溯、可审计的流程。哪怕没有自动化框架只要有一批可执行脚本Shell就能先把这层保障兜住。1.2 Shell的角色不是测试框架而是流程调度器注意区分概念。Shell不负责“断言”这件事——断言在用例脚本内部完成Shell负责的是三件事把用例按顺序拉起来、把每个用例的退出码和日志捡起来、最后把结果拼成一份报告。你可以把Shell理解为流水线上的机械臂它不生产零件测试逻辑但负责抓取、传送和质检登记。这也是为什么很多重量级测试框架本身也要依赖Shell来启动、传参和汇总。做自动化测试这么多年我的一个体会是不要一上来就上重型平台先用Shell脚本把流程串起来等跑顺了再决定要不要迁到平台成本最低也最容易让团队接受。Shell本质上是“最小可用且永远不过时”的一环——一条命令就能跑、不用装额外运行库、放进Git里可以直接diff追溯、所有Linux/Mac环境都能用。相比之下直接引入一套测试平台学习成本、维护成本、权限管理都要跟着上来很多小团队根本没有余力养这套东西。先Shell后平台这条路线踩坑最少。2. 测试用例的组织方式让脚本能“认路”2.1 一个约定优先的用例目录结构批量执行的前提是“机器能找得到用例”。所以目录结构必须稳定、可预测。我常用的结构是这样testcases/ ├── t001_login_api.sh ├── t002_register_api.sh ├── ui/ │ └── t003_homepage.sh └── android/ └── t004_smoke_adb.sh根目录下按模块放文件夹用例文件统一用.sh后缀前面带编号。总控脚本里一个find testcases -type f -name *.sh | sort就把全部用例抓出来了。sort保证执行顺序稳定——这一点在功能测试里很重要某些用例依赖前面的数据准备场景顺序一变结果就飘。虽然理想状态是用例完全独立但现实项目里总有先后依赖文件名编号排序是最简单的兜底。这套结构还有一个好处新增用例的成本极低。随便一个组员按照约定在对应目录下放一个sh文件主流程一行都不用改。如果目录结构不统一总控脚本就得加各种特殊情况判断维护成本立刻上来了。约定优先于配置这句话在测试工程里同样成立。2.2 用例文件的成活规则退出码、输出、自包含批量执行有个前提每个用例脚本必须遵守三条约定。第一以退出码作为唯一判定标准。通过就exit 0失败就exit 1或非0断言逻辑写在脚本内部。第二stdout是可解析的日志不要往屏幕上打印一堆交互提示更不要让脚本停下来等人按回车。第三用例自包含执行前的前置条件要么自己处理要么通过显式的前置脚本准备不能依赖人提前点开某个环境。这三条缺一条批量跑就会变成批量翻车而且翻车后很难定位。举个反例有人把用例写成echo test ok结尾不管你实际接口是否通过都返回0跑批全绿报告漂亮上线就出事。退出码这个约定是批量执行的生命线宁可严格到“断言失败必须非0退出”也不要图省事。我只验证用例脚本时第一件事就是手动执行后看echo $?是不是符合预期不符合的一律不纳入跑批。2.3 用Shell顺手整理旧用例批量重命名与归档有了一批老用例编号或前缀不统一怎么办用Shell一行搞定。例如把旧的t1_login.sh、t99_xxx.sh统一改成带前缀的规范名称for f in testcases/t*.sh; do [ -e $f ] || continue mv $f testcases/tc_$(basename $f) done如果想在旧编号前加上日期前缀做归档版本也可以改成这样stamp$(date %Y%m%d) for f in testcases/*.sh; do [ -e $f ] || continue mv $f ${f%.sh}_${stamp}.sh done${f%.sh}是Shell的参数展开去掉后缀再拼日期。这种操作在批量执行前做一次整个目录就变得规整。注意一点重命名后如果其他脚本里写死了旧路径要全局搜索替换别光改文件名忘了引用。顺带一提很多项目里的用例脚本是README和用例混在一起放批量跑之前一定要先把目录清理干净不然find会把说明文档也当成用例执行。3. 批量执行脚本的实现从顺序到并发3.1 顺序执行版先把链路跑通下面是总控脚本run_tests.sh的最小可用版本它一次性把找用例、执行、判结果、写日志四件事做完。写这段的时候我有意用while IFS read -r而不是for file in $(find ...)后者遇到带空格的路径会把一个文件名拆成两半这个坑在后面避坑章节还会专门讲这里先按这个写法走对带空格路径的项目尤其友好#!/usr/bin/env bash set -uo pipefail TEST_DIRtestcases LOG_DIRresult mkdir -p $LOG_DIR while IFS read -r case_file; do case_name$(basename $case_file) echo 执行 $case_name start_time$(date %s) if timeout 120 bash $case_file $LOG_DIR/${case_name}.log 21; then statusPASS else statusFAIL fi end_time$(date %s) cost$((end_time - start_time)) log_summary$(head -n 1 $LOG_DIR/${case_name}.log | tr \n ) echo $status|$case_name|${cost}s|$case_file|$log_summary $LOG_DIR/summary.txt echo $status 耗时 ${cost}s done (find $TEST_DIR -type f -name *.sh | sort)这段脚本已经把核心闭环跑通了找用例、执行、判退出码、记日志、写结果。timeout 120的意思是单条用例最多跑120秒超过就强制终止并返回失败避免一条卡死拖垮整个批次。注意bash $case_file不要求用例文件有执行权限这对从Windows拷贝过来的项目特别友好。summary.txt里每一行是一个用例的结论用竖线分隔状态、用例名、耗时、路径、日志首行。这个格式是后面所有报告的数据基础字段顺序一开始就要定好后面加东西可以往后追加但前几列的约定不要乱改。3.2 并发执行版wait -n控制并发度顺序执行简单但接口用例动辄几十上百条一条3秒100条就要5分钟确实有点浪费时间。提高吞吐的办法是并发。Shell最朴素的并发模型是用把任务丢到后台再用wait回收。但无脑会瞬间拉起几十个进程把测试环境打挂。所以要限制并发数。run_one() { local case_file$1 local case_name; case_name$(basename $case_file) local log_file$LOG_DIR/${case_name}.log local start_time; start_time$(date %s) if timeout $TIMEOUT bash $case_file $log_file 21; then local statusPASS else local statusFAIL fi local end_time; end_time$(date %s) local cost$((end_time - start_time)) local log_summary log_summary$(head -n 1 $log_file | tr \n ) echo $status|$case_name|${cost}s|$case_file|$log_summary $LOG_DIR/summary.txt } MAX_PARALLEL4 active0 while IFS read -r case_file; do run_one $case_file active$((active 1)) if [ $active -ge $MAX_PARALLEL ]; then wait -n || true active$((active - 1)) fi done (find $TEST_DIR -type f -name *.sh | sort) waitwait -n是bash 4.3之后引入的特性意思是“等任意一个后台任务结束”。配合active计数就能把并发数稳定压在4个以内。Mac自带的bash是3.2不支持这个参数Mac用户要么升级bash要么换成xargs -P 4方案find ... -print0 | xargs -0 -P 4 -I {} bash -c run_one $1 _ {}。两种都能用自己按环境选。我日常更喜欢wait -n因为逻辑直观便于在函数里加自定义统计。并发数不是越大越好。接口测试改4个并发通常没问题但如果用例要操作同一个数据库里的同一条记录并发起来互相锁表失败率飙升。这种情况你要么串行要么把数据错开。实践经验是先串行跑一遍确认用例都稳定再上并发并发度从2开始往上试找到稳定阈值。3.3 超时保护、结果收集与失败重跑超时必须做否则一条用例连数据库挂了不返回整个批跑卡死。timeout的典型退出码是124我通常会把超时单独拎出来标记而不是简单归为FAIL这样报告里一眼能看出是“用例本身失败”还是“环境卡死/超时”。判断方式是if timeout $TIMEOUT bash $case_file $log_file 21; then statusPASS else code$? if [ $code -eq 124 ]; then statusTIMEOUT else statusFAIL fi fi顺手提醒一句Mac上没有timeout命令只有gtimeout那是装了coreutils之后才有的。如果你在Mac上跑这套脚本要么改命令名要么在脚本开头做一个探测没有timeout就从PATH里找gtimeout。另一个实用功能是失败重跑。正式跑完一轮把失败用例收集到文件然后单独重跑这些失败用例节省大量时间grep ^FAIL| $LOG_DIR/summary.txt | cut -d| -f4 $LOG_DIR/fail_list.txt if [ -s $LOG_DIR/fail_list.txt ]; then echo 重跑失败用例 while IFS read -r case_file; do run_one $case_file done $LOG_DIR/fail_list.txt fi这里-s判断文件非空避免全部通过时白跑一圈。我自己的习惯是第一轮失败先看日志能确认是环境波动就重跑确认是bug就让开发修修完再跑失败子集整套动作全在终端里完成效率比一遍遍全量回归高得多。4. 测试报告自动生成让结果可交付4.1 中间结果文件后续所有报告的地基执行过程中每跑完一条就往summary.txt追加一行格式是PASS|t001_login_api.sh|3s|./testcases/t001_login_api.sh|登录接口返回200。这个设计有讲究各个字段用|分隔便于grep、cut、awk做二次处理。它既是日志又是报告的数据源还是一个简易的持久化结果。不要在执行完再临时去解析一堆log文件那样既慢又容易漏数据。统计代码很简单total$(wc -l $LOG_DIR/summary.txt) passed$(grep -c ^PASS| $LOG_DIR/summary.txt || true) failed$(grep -c ^FAIL| $LOG_DIR/summary.txt || true) timed_out$(grep -c ^TIMEOUT| $LOG_DIR/summary.txt || true)注意最后三个命令都加了|| true因为grep没匹配到任何行时返回1如果脚本开了set -e它会让整段统计直接退出这是个特别容易踩的坑。我暂时不开set -e原因后面讲。4.2 HTML报告的模板与动态插行生成HTML报告我的做法是分成三块头部模板、每用例一行、尾部模板。头部模板用单引号heredoc完全不做变量展开保证结构稳定REPORT_HTMLreports/index.html mkdir -p reports cat $REPORT_HTML EOF !DOCTYPE html html langzh-CN head meta charsetUTF-8 title自动化测试报告/title style body { font-family: system-ui, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } .pass { color: #198754; } .fail, .timeout { color: #dc3545; } /style /head body h1自动化测试报告/h1 table trth用例/thth结果/thth耗时/thth日志摘要/th/tr EOF然后读取summary.txt每一行动态追加表格记录。日志里的特殊符号要先做HTML转义不然用例输出里如果带尖括号会把页面结构搞乱。转义函数和循环如下html_escape() { sed s//\amp;/g; s//\lt;/g; s//\gt;/g } while IFS read -r line; do status$(echo $line | cut -d| -f2) case_name$(echo $line | cut -d| -f3) cost$(echo $line | cut -d| -f4) summary$(echo $line | cut -d| -f5- | html_escape) summary${summary:0:200} printf tr class%std%s/tdtd%s/tdtd%s/tdtd%s/td/tr\n \ $status $case_name $status $cost $summary $REPORT_HTML done $LOG_DIR/summary.txt超长日志只截前面200字符够定位就够了。日志摘要用的是cut -d| -f5-不是-f5因为日志内容里可能也出现竖线取第5列及之后的所有内容才是完整摘要。这个细节不处理报告里的日志摘要会被截断得莫名其妙。最后追加统计行和闭合标签cat $REPORT_HTML EOF /table p总计${total}通过${passed}失败${failed}超时${timed_out}通过率${rate}%/p /body /html EOF这样生成出来的HTML干净、能在浏览器直接打开、可以发给同事也方便后续脚本抓取。4.3 报告归档、JSON摘要与文本日报同一天可能跑好几轮报告文件名必须带时间戳否则就互相覆盖。归档动作移到脚本末尾stamp$(date %Y%m%d_%H%M%S) mv $REPORT_HTML reports/test_report_${stamp}.html除了HTML我还会同时导出一份JSON摘要目的不是给人看而是给CI解析。Jenkins、GitLab这些流水线拿到JSON后可以直接读通过率、失败数、失败用例列表出产线闸门或告警。生成片段rate0 if [ $total -gt 0 ]; then rate$(awk BEGIN{printf \%.1f\, $passed * 100 / $total}) fi cat reports/summary_${stamp}.json EOF { timestamp: ${stamp}, total: ${total}, passed: ${passed}, failed: ${failed}, timeout: ${timed_out}, rate: ${rate} } EOF最后如果只是想在群里发一条执行结果日报纯文本比HTML更省事。用cat拼一个可直接复制的文本块cat EOF 测试执行完成 时间: ${stamp} 总计: ${total} 通过: ${passed} 失败: ${failed} 通过率: ${rate}% 失败用例: $(grep ^FAIL| $LOG_DIR/summary.txt | cut -d| -f3) EOF这个文本块可以直接贴进IM群或邮件大家不用开文件就能看到结论。我实际用下来文本日报的传播效率比HTML高得多因为微信、钉钉、邮件都能直接展示HTML反而不方便。5. 写Shell跑批脚本那些常见的坑5.1 CRLF、glob、IFS与路径空格先讲最经典的坑从Windows拷贝或编辑的脚本换行符是CRLFLinux执行时会报$\r: command not found。处理方法是sed -i s/\r$// run_tests.sh或者统一用dos2unix。我一般会在项目里放一个fix_line.sh专门批量处理所有脚本避免新同学拷贝过来莫名其妙报错。再讲路径空格。for f in $(find ...)这种写法遇到My Test这种带空格的目录会把一个路径拆成两个参数脚本直接崩。所以代码里凡是遍历都要用while IFS read -r加 (find ...)的组合并且所有变量使用处都要加双引号。还有glob没匹配到的情况比如for f in testcases/*.sh目录里一个sh都没有时$f会保留为字面testcases/*.sh。处理办法是在循环里判断文件是否存在或者开nullglob。这些细节看着小踩一次能浪费半小时。5.2 if [ -n $var ] 与set -e的配合判断变量是否非空标准写法是if [ -n $var ]。很多人省略引号写成[ -n $var ]变量为空时$var被展开成空串后命令变成了[ -n ]判断恒为真逻辑直接反了。这个引号千万不能省。另一个相关坑是默认值比如读取环境变量timeout_sec${TIMEOUT_SEC:-120}:-表示变量为空或未设置时用默认值这在脚本需要兼容不同环境时非常实用。set -e同样是个双刃剑。它能让“某条命令失败就立即退出”防止脚本带着错误继续跑。但副作用是grep查不到内容、curl连接失败、甚至wait遇到非0退出码都会让整个脚本中断。所以我在跑批脚本里通常用set -uo pipefail不盲目开set -e然后自己在关键步骤手动判断退出码、手动决定是否退出。这样可控性最好也最容易排查问题。5.3 export与作用域多脚本协作的变量传递跑批脚本往往不是单文件你可能有一个env.sh放公共配置。子脚本要拿到这些变量必须用export导出否则子进程环境里根本没有这个变量export BASE_URLhttp://staging.example.com export TIMEOUT_SEC60 ./run_one_case.sh相反子脚本里修改的变量不会影响父脚本因为每个脚本是独立进程。即使你在子shell里设置export比如(export FOObar; echo $FOO)括号执行完父shell照样拿不到FOO。如果确实需要把子脚本的计算结果带回来就别用子shell用source让脚本在当前进程里执行或者让子脚本把结果写入文件再由父脚本读取。我倾向于后者文件传参最简单、最容易追溯出问题也能直接看文件内容。5.4 shift解析参数让脚本看起来像正经工具跑批脚本如果只写死参数换目录就要改代码太不优雅。用shift把位置参数一个一个挪走再配合case匹配就能做出标准命令行工具的感觉usage() { echo 用法: $0 [-t 超时秒数] [-p 并发数] [-d 用例目录] [-h 帮助] } while [ $# -gt 0 ]; do case $1 in -t|--timeout) TIMEOUT$2; shift 2;; -p|--parallel) MAX_PARALLEL$2; shift 2;; -d|--dir) TEST_DIR$2; shift 2;; -h|--help) usage; exit 0;; *) echo 未知参数: $1 2; usage; exit 1;; esac doneshift 2的意思是消费掉一个参数名和它的值$3变成新的$1。这样调用方式就变成了./run_tests.sh -t 60 -p 4 -d testcases别人一看用法就明白不用翻源码。如果参数后面跟的值没传比如只写了-t而没有数字脚本会静默拿到空值严谨一点可以在解析前判断[ -z ${2:-} ]并报错。给脚本写好参数解析是Shell从“临时脚本”进化成“团队工具”的分水岭。6. 从本地跑批到更大场景的扩展6.1 接入CI流水线的基本思路本地脚本跑顺之后接入CI是很自然的事。思路很简单总控脚本保持幂等——跑多少次结果都可靠、不依赖本地残留状态脚本末尾根据失败用例数决定退出码失败数超过阈值就exit 1让流水线红灯拦发布。报告HTML和JSON作为流水线产物归档。我习惯把脚本写成“同一个命令本地和流水线都可以调用”只通过环境变量区分环境地址和测试范围。比如STAGING_URL、TEST_FILTER这些本地跑用开发环境流水线里自动取对应环境变量。还要注意一点流水线环境通常是干净的不像你本地可能有各种临时配置。所以脚本里所有依赖的路径、工具、权限都要做显式检查。比如command -v python3 || echo 缺少python3再退出总比跑一半报“找不到命令”直观得多。6.2 与Playwright、接口测试协同Shell跑批不只跑Shell用例它还能调度更重的测试工具。比如Playwright用例完全可以做成一个run.sh包装先启动服务、再跑npx playwright test、最后把退出码原样返回。这样Shell的总控脚本不需要关心内部是什么框架只负责把“这个用例”拉起来、收集退出码、写报告。对于接口测试也一样用例内部可以是一整段curl断言脚本。这种分层设计的好处是主流程极简具体业务的复杂度被隔离在每一个用例脚本里新加一个用例等于新加一个文件不用动总控团队协作成本很低。如果你的项目已经有Playwright而且它自己会生成HTML报告Shell脚本还可以负责把多个测试分片报告合并成一个总体报告按模块、按浏览器维度汇总这个场景Shell同样顺手。6.3 adb shell移动端测试的批量执行移动端场景下Shell依然是主力。adb shell命令可以在设备上执行命令比如安装包相关操作、启动应用、跑monkey、执行instrumentation测试。我的做法是把待执行的操作写成一个脚本文件push到设备的/data/local/tmp/下再通过adb shell sh /data/local/tmp/run_test.sh执行。整个流程完全可以纳入前面那套批量执行框架只是每个用例脚本内部不再是curl而是adb命令。有一个细节要提醒adb shell命令本身的返回码不一定等于设备上命令的返回码老版本平台经常把错误吞掉。稳妥的做法是在设备端把关键步骤结果写到临时文件再拉回到宿主机解析或者让adb执行命令时附带echo EXIT$?最后在宿主机grep这个值。掌握这一手Android设备上的冒烟测试和回归测试也能纳入同一套Shell批量执行体系里。这套东西我实际搭了不止一次最大的感受是脚本短、注释写清“为什么”、每半年重构一次因为用例数量涨上去之后并发、超时、报告结构都要跟着调。真把这套流程跑下来你会发现自己从“点鼠标的测试”变成了“设计流程的测试”这个转变的起点就是学会用Shell把批量执行和测试报告这两件事变成家常便饭。
返回列表