放弃第三方框架,用系统自带工具玩转 Shell 测试

原始文章出处: Shell Tool Testing

本文介绍一套无第三方依赖、基于 Shell + prove 命令 + TAP 协议 的 Shell 脚本 / 命令行工具自动化测试方案,对标 Python unittest 测试体验,适配类 Unix 系统,极简易扩展。

一、方案诞生背景

  1. 作者推崇自动化测试,依赖用例复现、逻辑调试能力
  2. Python 项目可直接使用内置 unittest,但日常更多开发 Shell 工具、编辑器插件辅助脚本
  3. 市面 Shell 测试框架多依赖第三方库,不想引入额外项目依赖
  4. 最终选用系统自带工具组合: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. 基础环境搭建

  1. 创建测试目录 t/ 统一存放测试脚本
  2. 编写 Shell 测试脚本,遵循 TAP 输出规范
  3. 自定义测试后缀(.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 优先最近失败用例,日常开发首选

七、方案整体优势

  1. 零依赖:仅依赖系统自带 Shell、Perl 环境,无需安装第三方组件
  2. 轻量化:测试模板代码极少,自定义公共脚本体积小巧
  3. 高灵活:纯 Shell 编写,可自由适配任意命令行工具测试
  4. 易集成:支持并行、重试、筛选失败用例等专业测试流程
  5. 易排错:失败日志信息完整,快速定位问题根源

八、总结

该套方案完美填补Shell 命令行工具自动化测试空白,兼顾简易性与专业性,既能满足小型工具快速编写测试用例,也可搭建完整规范大型测试套件,是类 Unix 环境下 Shell 项目自动化测试最优轻量方案。

相关推荐
红茶要加冰1 小时前
九、文本处理三剑客——sed
linux·运维·服务器·正则表达式·shell
红茶要加冰1 天前
五、流程控制之循环
linux·运维·shell
红茶要加冰1 天前
二、shell中的变量
linux·运维·shell
Irene19911 天前
大数据开发(Hadoop/Spark 生态)在 Ubuntu 环境下:5 个高频率使用的功能性 Shell 脚本
shell
Irene19911 天前
(课堂笔记)Shell 基础入门:语言特点、文件结构、变量定义与引用、循环、脚本调用、入参等
shell
技术落地手记2 天前
把AI塞进测试环节,我踩出了一条能用的路
人工智能·测试
大飞记Python2 天前
从“驱动地狱”到一行代码:WebDriverManager使用手记(附模板)
python·测试
甜甜圈圈子3 天前
JMeter开启TLSv1.3进行性能测试
测试
月読h3 天前
[Python]发送测试报告-DingTalkRobot&Email
测试