# Bats Shell Testing

> 何时使用

- Skill: `findscripter/bats-shell-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add findscripter/bats-shell-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/findscripter/bats-shell-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: findscripter (https://skillmd.com/u/findscripter)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/findscripter/bats-shell-testing

---

## 何时使用

- **该用**：给 shell 脚本/CLI 工具写单元测试；对脚本做 TDD（先写 `@test` 再实现）；在 CI/CD 中接入自动化脚本测试；覆盖边界与错误分支（缺参、文件不存在、权限拒绝、非法选项）；验证脚本在 bash/sh/dash 等多种 shell 下行为一致。
- **不该用（负边界）**：项目根本不含 shell 脚本；需要跨服务/真实环境的集成测试（Bats 只测 shell 层行为）；目标只是 lint 或格式化；只想做静态检查——那用 shellcheck（见互见），它不替代运行时测试。

## 步骤

1. **装 Bats 并确认目标 shell**：`brew install bats-core` / `npm i -g bats` / 源码 `./install.sh /usr/local`；`bats --version` 验证。先确认要支持的 shell 方言与环境。
2. **搭测试结构**：脚本放 `bin/`，测试放 `tests/*.bats`，夹具放 `tests/fixtures/`，共享工具放 `tests/test_helper.sh`（用 `load test_helper` 引入）。
3. **写测试三类断言**：退出码（`$status`）、输出（`$output` / `${lines[N]}`）、副作用（文件是否生成/内容/权限）。每个测试只验一件事，命名清楚说明意图。
4. **加 setup/teardown**：`setup` 建临时目录与夹具，`teardown` 清理；昂贵的一次性准备用 `setup_file`/`teardown_file`。
5. **隔离外部依赖**：mock 函数或在 `PATH` 前置 stub 目录拦截 `curl`/`jq` 等命令；缺依赖用 `skip`。
6. **跑测试并接 CI**：本地 `bats tests/*.bats`（`--tap` 出 TAP、`--parallel N` 并行），在 GitHub Actions / Makefile 中固化。

## 指令

**核心 API（背下来）**

- `run cmd` 执行命令并捕获结果 → 读 `$status`（退出码）、`$output`（全部输出）、`${lines[i]}`（按行）。
- `@test "描述" { ... }` 定义一个测试；测试体内任一 `[ ... ]` 失败即整测试失败。
- `setup`/`teardown` 每个测试前后各跑一次；`setup_file`/`teardown_file` 整文件一次。
- `load 文件名` 引入 helper；`skip "原因"` 跳过；`${BATS_TEST_DIRNAME}` 指向当前 .bats 所在目录。

**断言惯用法**

- 退出码：`[ "$status" -eq 0 ]` / `[ "$status" -ne 0 ]` / 指定码 `[ "$status" -eq 127 ]`。
- 输出相等/含子串/正则：`[ "$output" = "expected" ]` / `[[ "$output" == *"world"* ]]` / `[[ "$output" =~ ^[0-9]{4}$ ]]`。
- 文件副作用：`[ -f file ]`、`[ "$(cat file)" = "..." ]`、`[ "$(wc -c < file)" -eq 5 ]`。

**夹具与隔离**

- 临时目录：`setup() { TEST_DIR=$(mktemp -d); export TEST_DIR; }` + `teardown() { rm -rf "$TEST_DIR"; }`，绝不污染工作区。
- 命令 stub：把可执行假命令写进 `$STUBS_DIR` 并 `export PATH="$STUBS_DIR:$PATH"`，控制其输出与退出码。
- 函数 mock：重定义同名函数 + `export -f`，让被测脚本调到假实现。

## 示例

最小测试文件（夹具 + 三类断言）：

```bash
#!/usr/bin/env bats
load test_helper

setup()    { TMPDIR=$(mktemp -d); export TMPDIR; }
teardown() { rm -rf "$TMPDIR"; }

@test "成功时返回 0" {
    run my_function "input"
    [ "$status" -eq 0 ]
}

@test "缺参时报错并提示 Usage" {
    run my_function
    [ "$status" -ne 0 ]
    [[ "$output" == *"Usage:"* ]]
}

@test "生成输出文件且内容正确" {
    my_function > "$TMPDIR/out.txt"
    [ -f "$TMPDIR/out.txt" ]
    [ "$(cat "$TMPDIR/out.txt")" = "expected content" ]
}
```

命令打桩（拦截外部 `curl`）：

```bash
create_stub() {            # 在 $STUBS_DIR 生成假命令
    cat > "$STUBS_DIR/$1" <<EOF
#!/bin/bash
echo "$2"
exit ${3:-0}
EOF
    chmod +x "$STUBS_DIR/$1"
}

@test "API 调用走桩" {
    create_stub curl '{ "status": "ok" }' 0
    run my_api_function
    [ "$status" -eq 0 ]
}
```

依赖缺失时跳过 + 多 shell 兼容：

```bash
@test "JSON 解析" {
    command -v jq >/dev/null || skip "jq 未安装"
    run my_json_parser '{"key":"value"}'
    [ "$status" -eq 0 ]
}

@test "脚本在 POSIX sh 下可运行" {
    sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}
```

CI 接入（GitHub Actions 片段）：

```yaml
- name: Install Bats
  run: npm install --global bats
- name: Run Tests
  run: bats tests/*.bats --tap | tee test_output.tap
```

## 注意事项

- **务必清理**：临时文件/目录一律在 `teardown` 中 `rm -rf`，否则测试间相互污染。改了权限做完即复原（如 `chmod 000` 测完 `chmod 644`）。
- **测好失败路径**：别只测 happy path——缺参、`/nonexistent` 文件、空输入、权限拒绝、非法选项都要覆盖，并断言错误信息（`*"not found"*`、`*"Usage:"*`）。
- **`run` 的边界**：`run` 会吞掉退出码（命令失败不会让测试自动失败），必须显式断言 `$status`；不需要捕获时也可直接跑命令让其非零退出令测试失败。
- **隔离单元**：mock/stub 外部命令，别在单测里打真实网络/数据库；复杂数据用 fixtures 文件提升可读性。
- **可移植性**：`stat -f`、`echo -e`、`{1..10}` 等并非各 shell 通用；要跨 dash/ash 验证就在对应 shell 实跑（容器：`alpine`=ash、`debian`=dash）。
- **速度**：测试要快，独立用例用 `bats --parallel N` 并行；不寻常的 setup 写注释说明。

## 互见

- requires：`bash-defensive-patterns` —— 先会写健壮 shell 脚本，才谈得上为其编写有意义的测试。
- related：`posix-shell-scripting`（被测脚本若要可移植，配套用 sh 方言测试）、`shellcheck-linting`（静态检查与 Bats 运行时测试互补，二者都进 pre-commit）。
- combines_with：`ci-cd-pipeline-builder` —— 把 `bats tests/*.bats --tap` 接入流水线，回归早发现。
- 参考：Bats-core 仓库 github.com/bats-core/bats-core、文档 bats-core.readthedocs.io、TAP 协议 testanything.org。

---
采编自 sickn33/antigravity-awesome-skills（MIT 许可）。

