AAS 实战:基于 bats-testing-patterns 技能构建生产级 Shell 脚本测试体系
【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills
Bats(Bash Automated Testing System)是面向 Shell 脚本的 TAP 兼容测试框架。本指南以 AAS(agentic-awesome-skills)仓库中 bats-testing-patterns 技能实现手册 为骨架,系统讲解安装、测试结构、断言、setup/teardown、Mock、Fixture、CI/CD 集成等完整测试模式。读完本文,你将掌握一套可直接复制的生产级 Shell 测试方案,并能基于本仓库的技能定义文件(SKILL.md)在自己的项目中落地 TDD 工作流。
一、技能定位:什么时候该用 Bats
在 AAS 仓库中,bats-testing-patterns技能以 Claude 技能(SKILL.md)与根目录镜像(skills/bats-testing-patterns/SKILL.md)两份形式存在,front matter 记录了元数据:risk: critical、source: community、date_added: "2026-02-27"。技能描述明确了适用面——为 Shell 脚本编写单元测试、实现脚本 TDD、在 CI/CD 中搭建自动化测试、覆盖边界与错误条件、跨 Shell 环境验证行为。
技能的使用边界划分得很清楚:
- 适用场景:为 Shell 脚本写单元测试;实现脚本驱动的 TDD;在 CI/CD 管道中搭建自动化测试;测试边界与错误条件;跨 Shell 环境验证行为。
- 不适用场景:项目不使用 Shell 脚本;需要超出 Shell 行为范围的集成测试;目标仅限 lint 或格式化。
技能执行指令给出五步工作流:确认 Shell 方言与支持环境 → 搭建带 helpers 和 fixtures 的测试结构 → 针对退出码、输出和副作用写测试 → 添加 setup/teardown 并在 CI 中运行 → 需要详细示例时打开resources/implementation-playbook.md。本文主体正是对该实现手册的完整展开。
二、Bats 基础:TAP 框架与安装方式
2.1 什么是 Bats
Bats(Bash Automated Testing System)是符合 TAP(Test Anything Protocol,测试任何事物协议)的 Shell 脚本测试框架,核心能力包括:
- 简单、自然的测试语法(
@test块) - 与 CI 系统兼容的 TAP 输出格式
- Fixtures 与 setup/teardown 支持
- 断言辅助函数
- 并行测试执行
TAP 协议的价值在于标准化:只要测试输出遵循 TAP 格式,Jenkins、GitHub Actions、GitLab CI 等系统都能直接解析通过/失败状态,无需为每种框架定制适配器。
2.2 安装方式
Bats 支持三种主流安装途径,按平台选择:
# macOS with Homebrew brew install bats-core # Ubuntu/Debian(源码编译安装) git clone https://github.com/bats-core/bats-core.git cd bats-core ./install.sh /usr/local # From npm (Node.js) npm install --global bats # Verify installation bats --version- Homebrew 途径适合 macOS 本地开发;
install.sh接受安装前缀参数(如/usr/local),适合 Linux 服务器或 Docker 镜像构建阶段;npm 全局安装(bats包)最为轻量,常用于 CI 环境——安装后务必用bats --version验证。
2.3 推荐目录结构
实现手册给出标准分层结构,将"被测代码"与"测试代码"严格隔离:
project/ ├── bin/ │ ├── script.sh │ └── helper.sh ├── tests/ │ ├── test_script.bats │ ├── test_helper.sh │ ├── fixtures/ │ │ ├── input.txt │ │ └── expected_output.txt │ └── helpers/ │ └── mocks.bash └── README.mdbin/:被测的 Shell 脚本本体;tests/*.bats:测试用例文件;tests/test_helper.sh:共享的辅助函数(断言、环境准备);tests/fixtures/:静态输入/期望输出数据;tests/helpers/mocks.bash:Mock 与 Stub 实现。
这种结构与 AAS 仓库自身的脚本组织方式一致——仓库根目录的 scripts/ 下集中放置真实 Shell 脚本(如 validate-links.sh、activate-skills.sh、validate-glossary.sh),便于为它们统一编写测试。
三、基本测试结构:@test 块与生命周期钩子
一个最简但完整的.bats文件如下:
#!/usr/bin/env bats # Load test helper if present load test_helper # Setup runs before each test setup() { export TMPDIR=$(mktemp -d) } # Teardown runs after each test teardown() { rm -rf "$TMPDIR" } # Test: simple assertion @test "Function returns 0 on success" { run my_function "input" [ "$status" -eq 0 ] } # Test: output verification @test "Function outputs correct result" { run my_function "test" [ "$output" = "expected output" ] } # Test: error handling @test "Function returns 1 on missing argument" { run my_function [ "$status" -eq 1 ] }关键语法要素:
@test "描述" { ... }:每个用例块,描述字符串会直接出现在测试报告中;setup()/teardown():在每个用例前后各执行一次,用于隔离环境;mktemp -d创建临时目录,teardown 中必须清理;load test_helper:加载同目录下的辅助脚本(等价于 source);run <command>:捕获命令的执行结果,将退出码存入$status、标准输出存入$output;- 断言本质就是
[ ... ]/[[ ... ]]条件表达式,失败即用例失败。
值得注意:run捕获的$output是合并后的标准输出(多行输出会用换行拼接),而$lines数组则按行切分,两者在不同场景下各有用途。
四、断言模式:退出码、输出与文件
4.1 退出码断言
退出码是 Shell 程序最核心的契约,Bats 通过run+$status直接验证:
#!/usr/bin/env bats @test "Command succeeds" { run true [ "$status" -eq 0 ] } @test "Command fails as expected" { run false [ "$status" -ne 0 ] } @test "Command returns specific exit code" { run my_function --invalid [ "$status" -eq 127 ] } @test "Can capture command result" { run echo "hello" [ $status -eq 0 ] [ "$output" = "hello" ] }-eq 0验证成功路径;-ne 0验证失败路径;- 精确断言特定退出码(如 127 表示命令未找到)可以捕捉"失败但失败方式不对"的回归;
- 同时断言
$status与$output,让成功路径的"返回值"也受控。
4.2 输出断言
输出断言覆盖精确匹配、子串匹配、正则匹配与多行匹配四种形态:
#!/usr/bin/env bats @test "Output matches string" { result=$(echo "hello world") [ "$result" = "hello world" ] } @test "Output contains substring" { result=$(echo "hello world") [[ "$result" == *"world"* ]] } @test "Output matches pattern" { result=$(date +%Y) [[ "$result" =~ ^[0-9]{4}$ ]] } @test "Multi-line output" { run printf "line1\nline2\nline3" [ "$output" = "line1 line2 line3" ] } @test "Lines variable contains output" { run printf "line1\nline2\nline3" [ "${lines[0]}" = "line1" ] [ "${lines[1]}" = "line2" ] [ "${lines[2]}" = "line3" ] }[ "$a" = "$b" ]:POSIX 精确字符串比较,必须引号包裹防止分词;[[ "$a" == *"sub"* ]]:bash 内置的 glob 子串匹配;[[ "$a" =~ regex ]]:正则匹配,适合验证格式(如年份^[0-9]{4}$);- 多行输出用
$output整体比较,或按行用$lines数组逐行断言——后者对"输出顺序敏感"的解析类函数尤其有用。
4.3 文件断言
Shell 脚本常以文件系统副作用作为结果,因此文件断言是重点:
#!/usr/bin/env bats @test "File is created" { [ ! -f "$TMPDIR/output.txt" ] my_function > "$TMPDIR/output.txt" [ -f "$TMPDIR/output.txt" ] } @test "File contents match expected" { my_function > "$TMPDIR/output.txt" [ "$(cat "$TMPDIR/output.txt")" = "expected content" ] } @test "File is readable" { touch "$TMPDIR/test.txt" [ -r "$TMPDIR/test.txt" ] } @test "File has correct permissions" { touch "$TMPDIR/test.txt" chmod 644 "$TMPDIR/test.txt" [ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ] } @test "File size is correct" { echo -n "12345" > "$TMPDIR/test.txt" [ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ] }- 用
-f、-r、-d等测试操作符验证存在性、可读性、目录性;首个用例在调用前先断言"文件尚不存在",能验证函数确实"创建"而非复用旧文件; - 权限断言
stat -f %OLp是macOS 语法;在 Linux 上需改为stat -c "%a"(输出644)。若测试需跨平台,可用uname分支,或优先选择[ -x ]、[ -r ]这类可移植测试操作符; - 文件大小用
wc -c < file计数,注意echo -n避免换行干扰计数。
五、setup/teardown 生命周期模式
5.1 基本模式:每用例独立隔离
#!/usr/bin/env bats setup() { # Create test directory TEST_DIR=$(mktemp -d) export TEST_DIR # Source script under test source "${BATS_TEST_DIRNAME}/../bin/script.sh" } teardown() { # Clean up temporary directory rm -rf "$TEST_DIR" } @test "Test using TEST_DIR" { touch "$TEST_DIR/file.txt" [ -f "$TEST_DIR/file.txt" ] }$BATS_TEST_DIRNAME是 Bats 内置变量,指向当前.bats文件所在目录,用它可以稳定定位被测脚本(../bin/script.sh),不依赖工作目录;source把被测函数直接注入当前 Shell 环境,测试里可直接调用函数而非子进程;- 每个用例都获得全新临时目录,互不污染。
5.2 带资源初始化:构建输入/输出环境
#!/usr/bin/env bats setup() { # Create directory structure mkdir -p "$TMPDIR/data/input" mkdir -p "$TMPDIR/data/output" # Create test fixtures echo "line1" > "$TMPDIR/data/input/file1.txt" echo "line2" > "$TMPDIR/data/input/file2.txt" # Initialize environment export DATA_DIR="$TMPDIR/data" export INPUT_DIR="$DATA_DIR/input" export OUTPUT_DIR="$DATA_DIR/output" } teardown() { rm -rf "$TMPDIR/data" } @test "Processes input files" { run my_process_script "$INPUT_DIR" "$OUTPUT_DIR" [ "$status" -eq 0 ] [ -f "$OUTPUT_DIR/file1.txt" ] }对于"读目录、写目录"的批处理类脚本,setup 中预置输入文件、导出输入/输出目录变量,用例内直接引用,避免在每个用例里重复铺陈环境。
5.3 全局模式:setup_file / teardown_file
当多个用例共享昂贵资源(大文件、数据库、网络连接)时,使用文件级钩子:
#!/usr/bin/env bats # Load shared setup from test_helper.sh load test_helper # setup_file runs once before all tests setup_file() { export SHARED_RESOURCE=$(mktemp -d) echo "Expensive setup" > "$SHARED_RESOURCE/data.txt" } # teardown_file runs once after all tests teardown_file() { rm -rf "$SHARED_RESOURCE" } @test "First test uses shared resource" { [ -f "$SHARED_RESOURCE/data.txt" ] } @test "Second test uses shared resource" { [ -d "$SHARED_RESOURCE" ] }setup_file()在整个文件所有用例前执行一次,teardown_file()在全部用例结束后执行一次;- 代价是用例之间共享状态、存在隐性依赖——只把"只读型"或"重建成本高"的资源放这里,可写资源仍应走每用例的 setup/teardown。
六、Mock 与 Stub 模式
6.1 函数级 Mock
针对"被测脚本内部调用的外部命令/函数",在测试 Shell 中重新定义同名函数并导出:
#!/usr/bin/env bats # Mock external command my_external_tool() { echo "mocked output" return 0 } @test "Function uses mocked tool" { export -f my_external_tool run my_function [[ "$output" == *"mocked output"* ]] }export -f将函数连同定义导出到子进程环境,保证被测脚本即使 fork 子 Shell 也能命中 Mock;- Mock 内部可自定义输出与退出码,从而驱动被测代码走不同分支。
6.2 命令级 Stub:PATH 注入
当被测脚本直接调用外部可执行文件(如curl、jq)时,用假命令抢占 PATH:
#!/usr/bin/env bats setup() { # Create stub directory STUBS_DIR="$TMPDIR/stubs" mkdir -p "$STUBS_DIR" # Add to PATH export PATH="$STUBS_DIR:$PATH" } create_stub() { local cmd="$1" local output="$2" local code="${3:-0}" cat > "$STUBS_DIR/$cmd" <<EOF #!/bin/bash echo "$output" exit $code EOF chmod +x "$STUBS_DIR/$cmd" } @test "Function works with stubbed curl" { create_stub curl "{ \"status\": \"ok\" }" 0 run my_api_function [ "$status" -eq 0 ] }create_stub是复用的桩工厂:接收命令名、输出内容、退出码三个参数(退出码默认 0);- 将 stub 目录置于
PATH最前,Shell 命令查找会优先命中桩; - 这是模拟 HTTP 客户端、数据库客户端等外部依赖的最可靠手段——无需安装真实服务。
6.3 变量级 Stub:环境变量覆盖
很多 Shell 程序的行为由环境变量驱动,直接构造环境即可测两条路径:
#!/usr/bin/env bats @test "Function handles environment override" { export MY_SETTING="override_value" run my_function [ "$status" -eq 0 ] [[ "$output" == *"override_value"* ]] } @test "Function uses default when var unset" { unset MY_SETTING run my_function [ "$status" -eq 0 ] [[ "$output" == *"default"* ]] }同一函数在"变量被覆盖"与"变量未设置"两种环境下分别断言,验证配置优先级逻辑(覆盖值 > 默认值)。
七、Fixture 管理
7.1 静态 Fixture 文件
复杂输入数据(JSON、CSV、长文本)不适合内联在用例里,应放入tests/fixtures/:
#!/usr/bin/env bats # Fixture directory: tests/fixtures/ setup() { FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures" WORK_DIR=$(mktemp -d) export WORK_DIR } teardown() { rm -rf "$WORK_DIR" } @test "Process fixture file" { # Copy fixture to work directory cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt" # Run function run my_process_function "$WORK_DIR/input.txt" # Compare output diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt" }- 通过
$BATS_TEST_DIRNAME/fixtures定位 fixture,避免硬编码绝对路径; - 先拷贝到临时工作目录再处理,防止污染源 fixture;
- 最终用
diff与期望输出逐字节比对——比字符串相等更严格(能暴露多余空白/换行差异)。
7.2 动态 Fixture 生成
当需要大批量或参数化数据时,用函数现场生成:
#!/usr/bin/env bats generate_fixture() { local lines="$1" local file="$2" for i in $(seq 1 "$lines"); do echo "Line $i content" >> "$file" done } @test "Handle large input file" { generate_fixture 1000 "$TMPDIR/large.txt" run my_function "$TMPDIR/large.txt" [ "$status" -eq 0 ] [ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ] }动态生成适合性能/容量类验证(如 1000 行大文件),同时用例末尾复核生成结果,保证测试前提自身可靠。
八、进阶模式
8.1 错误条件测试
生产级测试必须覆盖失败路径,而不只是 happy path:
#!/usr/bin/env bats @test "Function fails with missing file" { run my_function "/nonexistent/file.txt" [ "$status" -ne 0 ] [[ "$output" == *"not found"* ]] } @test "Function fails with invalid input" { run my_function "" [ "$status" -ne 0 ] } @test "Function fails with permission denied" { touch "$TMPDIR/readonly.txt" chmod 000 "$TMPDIR/readonly.txt" run my_function "$TMPDIR/readonly.txt" [ "$status" -ne 0 ] chmod 644 "$TMPDIR/readonly.txt" # Cleanup } @test "Function provides helpful error message" { run my_function --invalid-option [ "$status" -ne 0 ] [[ "$output" == *"Usage:"* ]] }错误用例同时断言退出码非零与错误信息内容(not found、Usage:),后者能防止"报错但信息误导"的劣质实现;权限错误用例记得在断言后恢复权限(chmod 644),避免污染后续用例。
8.2 依赖工具检测与 skip
当被测脚本依赖jq等可选工具时,用skip优雅降级而非直接失败:
#!/usr/bin/env bats setup() { # Check for required tools if ! command -v jq &>/dev/null; then skip "jq is not installed" fi export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh" } @test "JSON parsing works" { skip_if ! command -v jq &>/dev/null run my_json_parser '{"key": "value"}' [ "$status" -eq 0 ] }skip "原因"使用例标记为跳过(TAP 输出中为ok # SKIP),CI 视为通过而非失败;- 对需要 root、特定架构、特定 Shell 方言的用例同样适用,让测试套件在不同环境都能跑起来。
8.3 跨 Shell 方言兼容性测试
Shell 脚本常需兼容 bash、POSIX sh、dash 等多种解释器:
#!/usr/bin/env bats @test "Script works in bash" { bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1 } @test "Script works in sh (POSIX)" { sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1 } @test "Script works in dash" { if command -v dash &>/dev/null; then dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1 else skip "dash not installed" fi }- 分别用
bash、sh、dash显式执行被测脚本,捕获"bash 特有语法在 POSIX 环境下崩溃"的移植性问题; dash不可用(如 macOS 默认无 dash)时跳过,保持跨平台可运行。
8.4 并行执行
对相互独立、互不共享状态的用例,可在 Bats 文件内并行化:
#!/usr/bin/env bats @test "Multiple independent operations" { run bash -c 'for i in {1..10}; do my_operation "$i" & done wait' [ "$status" -eq 0 ] } @test "Concurrent file operations" { for i in {1..5}; do my_function "$TMPDIR/file$i" & done wait [ -f "$TMPDIR/file1" ] [ -f "$TMPDIR/file5" ] }- Shell 层面用
&后台 +wait聚合,验证并发场景的正确性(如并发写不同文件); - 前提是各操作必须无共享可写状态——这要求测试数据隔离(每个任务使用独立文件),否则会产生竞态噪声。
九、Test Helper 复用模式
当多个.bats文件需要相同断言时,抽到test_helper.sh统一维护:
#!/usr/bin/env bash # Source script under test export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin" # Common test utilities assert_file_exists() { if [ ! -f "$1" ]; then echo "Expected file to exist: $1" return 1 fi } assert_file_equals() { local file="$1" local expected="$2" if [ ! -f "$file" ]; then echo "File does not exist: $file" return 1 fi local actual=$(cat "$file") if [ "$actual" != "$expected" ]; then echo "File contents do not match" echo "Expected: $expected" echo "Actual: $actual" return 1 fi } # Create temporary test directory setup_test_dir() { export TEST_DIR=$(mktemp -d) } cleanup_test_dir() { rm -rf "$TEST_DIR" }assert_file_exists/assert_file_equals返回非零即失败,并打印诊断信息(期望值 vs 实际值),失败可读性远高于裸[ ];SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"的推导逻辑与 AAS 仓库真实脚本的做法一致——例如 scripts/validate-links.sh 使用SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"再结合PROJECT_ROOT定位项目根,两者都是"基于脚本自身位置定位资源"的稳健模式;- 在测试文件顶部
load test_helper即可复用全套工具,保持用例简洁。
十、CI/CD 集成
10.1 GitHub Actions 工作流
name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Bats run: | npm install --global bats - name: Run Tests run: | bats tests/*.bats - name: Run Tests with Tap Reporter run: | bats tests/*.bats --tap | tee test_output.tapnpm install --global bats一行完成 CI 安装,无需克隆源码;bats tests/*.bats通配符批量执行全部测试文件,非零退出码自动使 CI 失败;--tap输出 TAP 格式并通过tee落盘存档,供后续解析或归档。
10.2 Makefile 集成
.PHONY: test test-verbose test-tap test: bats tests/*.bats test-verbose: bats tests/*.bats --verbose test-tap: bats tests/*.bats --tap test-parallel: bats tests/*.bats --parallel 4 coverage: test # Optional: Generate coverage reports--verbose:打印每个用例的详细输出(含run捕获的 stdout/stderr),排查失败更直观;--parallel 4:Bats 内置并行执行(按文件粒度并行,4 路并发),前提是各测试文件间无共享状态;coverage目标预留为覆盖率报告的扩展点。
十一、最佳实践十条
实现手册沉淀了十条可操作的工程准则,逐条落地即可让测试套件达到生产级:
- 每个测试只测一件事——单一职责原则,失败定位一目了然;
- 使用描述性测试名——
@test的描述要清楚说明被测行为; - 测试后清理——所有临时文件必须在 teardown 中移除,避免跨用例污染与磁盘泄漏;
- 同时测成功与失败路径——不要只写 happy path,错误处理是 Shell 脚本最易回归的部分;
- Mock 外部依赖——用函数 Mock、PATH 桩、环境变量隔离被测单元,保证测试确定性;
- 复杂数据用 Fixtures——长 JSON/CSV 放入
tests/fixtures/,提升可读性; - 在 CI/CD 中运行测试——尽早捕获回归;
- 跨 Shell 方言测试——bash/sh/dash 分别验证,确保可移植性;
- 保持测试快速——能并行就并行,慢测试会拖垮开发节奏;
- 文档化复杂测试配置——对非常规模式(如桩、共享资源)加注释说明。
十二、在 AAS 仓库中的落点与延伸
本技能在仓库中有两个实体:技能定义与使用说明 负责"何时用、怎么用"的决策层,实现手册 负责"怎么写"的细节层,两者配合构成完整技能;根目录另有镜像副本 skills/bats-testing-patterns/SKILL.md。
作为实践参照,AAS 仓库自身的 scripts/ 目录就是天然的 Bats 测试对象:以 validate-links.sh 为例,它具备典型的生产 Shell 脚本特征——set -euo pipefail严格模式、基于BASH_SOURCE推导脚本目录、路径感知的确定性输出,这些都可以用本文的退出码断言、输出断言与文件断言体系逐一覆盖;activate-skills.sh、validate-glossary.sh 同理。你可以在自己的项目中按第二节的目录结构搭建tests/树,配合test_helper.sh与fixtures/,将本文全部模式直接落地。
【免费下载链接】agentic-awesome-skillsAAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and planning, backed by 2,445+ agentic skills. Includes CLI, local MCP, catalog, plugins, and Workbench.项目地址: https://gitcode.com/gh_mirrors/an/agentic-awesome-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考