原始文章出处: Shell Tool Testing
本文介绍一套无第三方依赖、基于 Shell + prove 命令 + TAP 协议 的 Shell 脚本 / 命令行工具自动化测试方案,对标 Python unittest 测试体验,适配类 Unix 系统,极简易扩展。
一、方案诞生背景
- 作者推崇自动化测试,依赖用例复现、逻辑调试能力
- Python 项目可直接使用内置
unittest,但日常更多开发 Shell 工具、编辑器插件辅助脚本 - 市面 Shell 测试框架多依赖第三方库,不想引入额外项目依赖
- 最终选用系统自带工具组合:prove 测试运行器 + TAP 测试结果协议
二、核心基础概念
1. prove 命令
- 来源:Perl 生态命令行测试执行工具
- 优势:跨语言通用,不限制测试脚本编程语言,主流类 Unix 系统默认预装
- 作用:自动匹配指定命名规则的测试文件、批量执行、汇总测试结果
使用示例:贴合本文测试场景,极简常用命令
- 直接运行默认规则测试文件:
prove- 指定识别.test后缀测试文件:
prove --ext test- 多线程并行执行测试:
prove -j 2执行后自动扫描
t/目录下测试脚本,解析TAP输出,打印美化后的测试通过率、失败统计。
2. TAP 测试协议(Test Anything Protocol)
- 定位:轻量纯文本测试结果输出规范,结构简洁易解析
- 基础格式:
- 声明协议版本
- 用例结果:
ok通过 /not ok失败 #开头为注释日志- 末尾声明总测试用例数
- 优势:比 JUnit XML 更轻量化,适配 Shell 极简输出
使用示例:Shell脚本原生输出标准TAP数据流(本文最简示例)
bash#!/bin/sh echo "TAP version 14" # 协议版本 echo "ok 1 - 测试目录创建" # 通过用例 echo "not ok 2 - 重复创建目录" # 失败用例 # 错误日志注释 # 声明总用例数量 echo "1..2"该文本无任何特殊语法,纯明文输出,
prove可直接识别解析,无需额外解析器。
三、从零搭建极简测试框架
1. 基础环境搭建
- 创建测试目录
t/统一存放测试脚本 - 编写 Shell 测试脚本,遵循 TAP 输出规范
- 自定义测试后缀(
.test),新建.proverc全局配置,免去每次输入参数
完整基础测试脚本(t/example.test)
bash
#!/bin/sh
# TAP 协议版本声明
echo "TAP version 14"
# 测试用例:通过
echo "ok 1 - an example test"
# 声明总测试用例数量
echo "1..1"
配置文件(.proverc)
指定 prove 识别.test后缀的测试文件:
bash
--ext test
2. 基础功能测试编写
以测试mkdir命令为例,原生 Shell 编写测试用例:
原生基础测试脚本(t/make-a-directory.test)
bash
#!/bin/sh
echo "TAP version 14"
# 测试1:执行mkdir命令
mkdir foo
if [ "$?" -eq 0 ]; then
echo "ok 1 - mkdir exit status"
else
echo "not ok 1 - mkdir exit status"
fi
# 测试2:校验目录是否创建成功
if [ -d foo ]; then
echo "ok 2 - directory created"
else
echo "not ok 2 - directory created"
fi
echo "1..2"
四、测试核心优化要点
1. 测试环境隔离
解决测试残留文件导致的用例失败,使用临时目录 + 自动清理:
环境隔离完整测试代码
bash
#!/bin/sh
echo "TAP version 14"
# 创建临时测试目录
TESTDATA=$(mktemp -d)
# 在临时目录内创建文件,避免环境干扰
mkdir "$TESTDATA"/foo
if [ "$?" -eq 0 ]; then
echo "ok 1 - mkdir exit status"
else
echo "not ok 1 - mkdir exit status"
fi
if [ -d "$TESTDATA"/foo ]; then
echo "ok 2 - directory created"
else
echo "not ok 2 - directory created"
fi
echo "1..2"
# 测试完成后清理临时目录
rm -rf "$TESTDATA"
2. 简化用例编写
封装工具函数,自动管理测试编号,无需手动修改:
封装函数后的完整测试代码
bash
#!/bin/sh
# 测试通过函数:自动编号+输出结果
report_ok() {
TESTCOUNT=$(( TESTCOUNT + 1 ))
echo "ok $TESTCOUNT - $*"
}
# 测试失败函数:自动编号+输出结果
report_not_ok() {
TESTCOUNT=$(( TESTCOUNT + 1 ))
echo "not ok $TESTCOUNT - $*"
}
echo "TAP version 14"
TESTDATA=$(mktemp -d)
TESTCOUNT=0
# 测试1:首次创建目录
mkdir "$TESTDATA"/foo
if [ "$?" -eq 0 ]; then
report_ok "mkdir exit status"
else
report_not_ok "mkdir exit status"
fi
# 测试2:校验目录存在
if [ -d "$TESTDATA"/foo ]; then
report_ok "directory created"
else
report_not_ok "directory created"
fi
# 测试3:重复创建目录(预期失败)
mkdir "$TESTDATA"/foo
if [ "$?" -eq 1 ]; then
report_ok "mkdir fails a second time"
else
report_not_ok "mkdir fails a second time"
fi
# 自动输出总用例数
echo "1..$TESTCOUNT"
rm -rf "$TESTDATA"
3. Shell 严格模式避坑
set -e会导致命令失败直接退出,兼容写法:
bash
EXIT_STATUS=0
mkdir "$TESTDATA"/foo || EXIT_STATUS="$?"
if [ "$EXIT_STATUS" -eq 1 ]; then
report_ok "mkdir fails a second time"
fi
4. 错误日志精细化管控
封装日志、命令记录、断言函数,优化失败日志输出:
bash
# 核心工具函数(后续会抽离为公共脚本)
log() { printf "# %s\n" "$*" 1>&2 ; }
record() {
echo "$*" > "$TESTDATA/last-command"
"$@" > "$TESTDATA/last-stdout" 2> "$TESTDATA/last-stderr"
echo "$?" > "$TESTDATA/last-exit"
}
report_ok() { TESTCOUNT=$(( TESTCOUNT + 1 )); echo "ok $TESTCOUNT - $*"; }
report_not_ok() {
TESTCOUNT=$(( TESTCOUNT + 1 ))
echo "not ok $TESTCOUNT - $*"
echo 1>&2
log "Test: $*"
log "Last command: $(cat "$TESTDATA/last-command")"
log "Exit status: $(cat "$TESTDATA/last-exit")"
}
# 断言退出码
assert_exit_status() {
expected="$1"
got="$(cat "$TESTDATA/last-exit")"
[ "$got" -eq "$expected" ] && report_ok "$*" || report_not_ok "$*"
}
五、模块化测试套件架构
1. 公共脚本抽离(核心)
将所有工具函数封装为t/common.sh,所有测试脚本复用:
完整公共脚本(t/common.sh)
bash
# 日志输出函数
log() { printf "# %s\n" "$*" 1>&2 ; }
# 记录命令执行结果
record() {
echo "$*" > "$TESTDATA/last-command"
"$@" > "$TESTDATA/last-stdout" 2> "$TESTDATA/last-stderr"
echo "$?" > "$TESTDATA/last-exit"
}
# 测试通过
report_ok() {
TESTCOUNT=$(( TESTCOUNT + 1 ))
echo "ok $TESTCOUNT - $*"
}
# 测试失败(输出详细日志)
report_not_ok() {
TESTCOUNT=$(( TESTCOUNT + 1 ))
echo "not ok $TESTCOUNT - $*"
echo 1>&2
log "Test: $*"
log "Last command: $(cat "$TESTDATA/last-command")"
log "Exit status: $(cat "$TESTDATA/last-exit")"
log "stdout was:"
sed -e 's/^/# /' "$TESTDATA/last-stdout" >&2
log "stderr was:"
sed -e 's/^/# /' "$TESTDATA/last-stderr" >&2
}
# 断言退出状态码
assert_exit_status() {
expected="$1"
shift
got="$(cat "$TESTDATA/last-exit")"
if [ "$got" -eq "$expected" ] ; then
report_ok "$*"
else
report_not_ok "$*"
log "Expected exit status $expected"
fi
}
# 断言错误输出匹配正则
assert_stderr_matches() {
pattern="$1"
shift
if grep -E "$pattern" "$TESTDATA/last-stderr" >/dev/null ; then
report_ok "$*"
else
report_not_ok "$*"
log "Expected stderr match: $pattern"
fi
}
# 测试初始化
setup() {
TESTDATA=$(mktemp -d)
TESTCOUNT=0
echo "TAP version 14"
}
# 测试清理
teardown() {
echo "1..$TESTCOUNT"
rm -rf "$TESTDATA"
}
# 紧急中断测试
bail_out() {
printf "Bail out! %s\n" "$*"
exit 1
}
# 初始化失败直接中断
run_or_bail() {
record "$@"
if [ "$(cat "$TESTDATA"/last-exit)" -ne 0 ]; then
log "Setup failed: $*"
bail_out "Cannot continue test"
fi
}
2. 模块化测试脚本(.test)
统一引入公共脚本,解决路径引用问题:
模块化基础测试(t/make-a-directory-modularised.test)
bash
#!/bin/sh
# 切换到脚本所在目录,解决公共文件引用问题
cd "$(dirname "$0")"
# 引入公共工具函数
. ./common.sh
# 初始化测试环境
setup
# 执行测试用例
record mkdir "$TESTDATA"/foo
assert_exit_status 0 "mkdir exit status"
if [ -d "$TESTDATA"/foo ]; then
report_ok "directory created"
else
report_not_ok "directory created"
fi
# 清理测试环境
teardown
重复创建目录测试(t/cannot-recreate-a-directory.test)
bash
#!/bin/sh
cd "$(dirname "$0")"
. ./common.sh
setup
# 初始化命令失败直接中断
run_or_bail mkdir "$TESTDATA"/foo
# 核心测试:重复创建预期失败
record mkdir "$TESTDATA"/foo
assert_exit_status 1 "mkdir fails a second time"
assert_stderr_matches "File exists" "mkdir failed for the right reason"
teardown
六、prove 高级高效运行技巧
| 执行参数 | 作用 |
|---|---|
prove -j 线程数 |
多线程并行运行测试,提速大批量用例 |
prove -s |
随机打乱测试执行顺序,排查隐性用例依赖 |
prove --state=slow |
优先运行耗时最长测试用例 |
prove --state=fast |
优先快速用例,快速获取变更反馈 |
prove --state=failed |
仅执行上一轮失败用例,精准调试 BUG |
prove --state=hot,all,save |
优先最近失败用例,日常开发首选 |
七、方案整体优势
- 零依赖:仅依赖系统自带 Shell、Perl 环境,无需安装第三方组件
- 轻量化:测试模板代码极少,自定义公共脚本体积小巧
- 高灵活:纯 Shell 编写,可自由适配任意命令行工具测试
- 易集成:支持并行、重试、筛选失败用例等专业测试流程
- 易排错:失败日志信息完整,快速定位问题根源
八、总结
该套方案完美填补Shell 命令行工具自动化测试空白,兼顾简易性与专业性,既能满足小型工具快速编写测试用例,也可搭建完整规范大型测试套件,是类 Unix 环境下 Shell 项目自动化测试最优轻量方案。