使用 Bats-core 对函数进行单元测试
组织测试文件、断言以及 setup/teardown,以验证单个 Bash 函数。
使用 Bats-core 对函数进行单元测试 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
Bats-core 是什么,为什么使用它?
Bats-core(Bash 自动化测试系统)是 Bash 事实上的单元测试框架。它可以让您以结构化、可重复的方式为 shell 函数和脚本编写测试,就像使用 JUnit 测试 Java 或使用 pytest 测试 Python 一样。
- 每个测试都是一个带有人类可读描述的
@test块。 - 只有块内的每条命令都返回退出码
0时,测试才会通过。 - 一旦出现非零退出码或断言失败,测试就会失败。
- 输出兼容 TAP,因此 CI 系统(GitHub Actions、Jenkins、GitLab CI)可以原生理解它。
您可以通过软件包管理器安装,或克隆代码仓库:
# Install via git (recommended — always latest)
git clone https://github.com/bats-core/bats-core.git
cd bats-core && sudo ./install.sh /usr/local
# Or on macOS with Homebrew
brew install bats-core
# Verify installation
bats --version
# bats 1.x.y您的第一个 Bats 测试文件
Bats 测试文件的扩展名为 .bats,并以特殊的 shebang 开头。其核心构建单元是 @test 指令,后面跟着描述字符串和一组命令。
- shebang
#!/usr/bin/env bats告诉 shell 如何执行该文件。 - 每个
@test块都是一个独立的测试用例。 - 您可以使用
bats my_tests.bats运行单个文件,也可以使用bats test/运行整个目录。
下面是 Bats 测试文件的最简结构:
#!/usr/bin/env bats
# File: test/hello.bats
@test "echo outputs the expected string" {
result=$(echo "hello world")
[ "$result" = "hello world" ]
}
@test "false command causes test to fail" {
# Uncommenting the next line would make this test fail:
# false
true
}使用“load”加载待测函数
在实际项目中,Bash 函数通常位于库文件中,而不是直接写在测试文件里。Bats 提供了 load 助手,用于相对于测试文件所在目录加载外部文件。
load '../lib/math.sh'会在每个测试运行前加载该文件。- 加载后,该文件中定义的所有函数都可以在测试块中使用。
- 请将库函数放在
lib/目录中,将测试放在test/目录中,以实现清晰分离。
下面是示例项目布局及相应的测试:
# Project layout:
# lib/math.sh <- functions to test
# test/math.bats <- test file
# lib/math.sh
add() {
echo $(( $1 + $2 ))
}
divide() {
if [ "$2" -eq 0 ]; then
echo "Error: division by zero" >&2
return 1
fi
echo $(( $1 / $2 ))
}
# test/math.bats
#!/usr/bin/env bats
load '../lib/math.sh'
@test "add returns correct sum" {
result=$(add 3 4)
[ "$result" = "7" ]
}核心断言:run、$status、$output
run 命令是 Bats 测试的核心。与直接执行命令不同,使用 run 包装命令可以捕获其退出码和输出,而不会导致测试立即失败。
$status— 保存上一个run命令的退出码。$output— 保存上一个run命令合并后的标准输出。$lines— 一个数组,其中每个元素都是一行输出(${lines[0]}、${lines[1]}等)。
这样,您就可以同时断言成功和失败的情况:
#!/usr/bin/env bats
load '../lib/math.sh'
@test "divide 10 by 2 returns 5" {
run divide 10 2
[ "$status" -eq 0 ]
[ "$output" = "5" ]
}
@test "divide by zero returns exit code 1" {
run divide 10 0
[ "$status" -eq 1 ]
}
@test "divide by zero prints error message" {
run divide 10 0
# $output captures stderr too when redirected inside the function
[[ "$output" == *"division by zero"* ]]
}使用 bats-assert 编写清晰易懂的断言
内置的 [ ] 断言可以使用,但失败信息不够清晰。bats-assert 助手库提供了表达力更强的断言函数,能够准确指出出错之处。
assert_success— 断言$status为 0。assert_failure— 断言$status为非零值。assert_output— 断言$output等于给定字符串。assert_output --partial— 断言输出包含该子字符串。refute_output --partial— 断言输出不包含该子字符串。
请将 bats-core/bats-assert 克隆到 test/helpers/ 文件夹中进行安装,然后加载它:
#!/usr/bin/env bats
# Load bats-assert (cloned into test/helpers/bats-assert)
load 'helpers/bats-assert/load'
load '../lib/math.sh'
@test "add 5 and 3 gives 8" {
run add 5 3
assert_success
assert_output "8"
}
@test "divide by zero fails with descriptive message" {
run divide 9 0
assert_failure
assert_output --partial "division by zero"
}
@test "add does not output an error" {
run add 1 1
refute_output --partial "Error"
}setup 和 teardown:测试生命周期钩子
Bats 提供了两个特殊函数——setup 和 teardown,它们会在每个测试前后自动运行。您可以使用它们准备和清理共享状态,确保每个测试都从已知环境开始。
setup()会在每个单独的@test块之前运行。teardown()会在每个单独的@test块之后运行,即使测试失败也不例外。- 常见用途包括:创建临时目录、设置环境变量,以及在测试后删除临时文件。
#!/usr/bin/env bats
load '../lib/fileutils.sh'
setup() {
# Create a fresh temp directory before every test
TEST_DIR=$(mktemp -d)
export TEST_DIR
}
teardown() {
# Always clean up, even on test failure
rm -rf "$TEST_DIR"
}
@test "write_file creates a file with correct content" {
run write_file "$TEST_DIR/hello.txt" "hello world"
assert_success
[ -f "$TEST_DIR/hello.txt" ]
[ "$(cat "$TEST_DIR/hello.txt")" = "hello world" ]
}
@test "write_file fails when directory does not exist" {
run write_file "/nonexistent/dir/file.txt" "data"
assert_failure
}setup_file 和 teardown_file:测试套件级钩子
有时,您只需要每个文件设置一次开销较大的资源,而不是在每个测试前都设置。Bats 为此提供了 setup_file 和 teardown_file。
setup_file()会在文件中的所有测试开始前运行一次。teardown_file()会在文件中的所有测试结束后运行一次。- 请使用
BATS_FILE_TMPDIR(会自动提供)在setup_file和测试之间共享数据——普通变量无法跨子 shell 持久存在。
典型用例是启动模拟服务器或构建一次二进制文件,然后在最后将其关闭:
#!/usr/bin/env bats
setup_file() {
# Build the project binary once for all tests in this file
make build --silent
export BINARY="$PWD/bin/myapp"
echo "Binary built: $BINARY"
}
teardown_file() {
# Remove the binary after all tests complete
rm -f "$BINARY"
echo "Cleaned up binary"
}
setup() {
# Still runs before each individual test
TEST_TMP=$(mktemp -d)
}
teardown() {
rm -rf "$TEST_TMP"
}
@test "myapp --version outputs version string" {
run "$BINARY" --version
assert_output --partial "1.0"
}测试会修改文件的函数
一种非常常见的模式是测试从文件系统读取数据或向文件系统写入数据的 Bash 函数。关键技术是使用临时目录(在 setup 中通过 mktemp -d 创建),这样测试就不会接触真实文件,也不会相互干扰。
- 始终在
$TEST_DIR中操作(或使用会在新版 Bats 中自动提供的$BATS_TEST_TMPDIR)。 - 使用
bats-file助手库执行清晰的文件断言,例如assert_file_exists和assert_file_contains。 - 不要硬编码
/tmp/myfile这样的路径——并行运行的测试会发生冲突。
#!/usr/bin/env bats
load 'helpers/bats-assert/load'
load 'helpers/bats-file/load'
load '../lib/fileutils.sh'
setup() {
TEST_DIR="$BATS_TEST_TMPDIR"
}
# lib/fileutils.sh defines:
# append_line() { echo "$2" >> "$1"; }
@test "append_line adds a line to an existing file" {
echo "first line" > "$TEST_DIR/log.txt"
run append_line "$TEST_DIR/log.txt" "second line"
assert_success
assert_file_contains "$TEST_DIR/log.txt" "second line"
}
@test "append_line creates file if it does not exist" {
run append_line "$TEST_DIR/new.txt" "hello"
assert_success
assert_file_exists "$TEST_DIR/new.txt"
}模拟外部命令
函数经常会调用 curl、aws 或 git 等外部程序。在单元测试中,您要测试的是自己的逻辑,而不是真实的外部命令。Bats 中最简洁的模拟方法,是在 setup 中定义一个与命令同名的 shell 函数——它会优先于真实的二进制文件。
- 在
setup中定义类似curl() { echo 'mocked response'; return 0; }的函数,并将其导出。 - 使用
export -f curl,使该函数在run创建的子 shell 中也可见。 - 对于更复杂的情况,也可以将模拟命令写入
PATH中的临时文件。
#!/usr/bin/env bats
load 'helpers/bats-assert/load'
load '../lib/network.sh'
# lib/network.sh defines:
# fetch_status() {
# local url="$1"
# local code
# code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
# echo "$code"
# }
setup() {
# Override 'curl' with a mock function
curl() {
# Simulate a 200 OK response
echo "200"
return 0
}
export -f curl
}
@test "fetch_status returns 200 when curl reports 200" {
run fetch_status "https://example.com"
assert_success
assert_output "200"
}跳过测试和添加标签
并非每个测试都能始终运行——有时您需要真实的网络连接、特定工具或某个操作系统。Bats 提供了 skip,可以使用说明性消息有条件地跳过测试,而不必将其注释掉或让整个测试套件中断。
- 在
@test块中的任意位置调用skip "reason",即可跳过该测试。 - 被跳过的测试会在输出中显示为
S,并且不会计为失败。 - Bats 1.5 及更高版本支持标签:使用
# bats test_tags=slow,network为测试添加注释,再使用bats --filter-tags network test/进行筛选。
#!/usr/bin/env bats
# bats test_tags=network
@test "API returns valid JSON" {
# Skip if no internet connectivity
if ! ping -c1 -W1 8.8.8.8 &>/dev/null; then
skip "No network connection available"
fi
run curl -s "https://api.example.com/health"
assert_success
assert_output --partial '"status"'
}
# bats test_tags=unit
@test "slug function lowercases and replaces spaces" {
# Always runs — pure function, no external deps
slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-'; }
run slug "Hello World"
assert_output "hello-world"
}
# Run only unit tests:
# bats --filter-tags unit test/构建完整的测试套件
组织良好的 Bats 项目会采用可预测的目录布局,从而便于新贡献者上手,并与 CI 流水线集成。
推荐的结构如下:
lib/— 生产环境使用的 Bash 函数(每个关注点一个文件:math.sh、fileutils.sh)。test/— 每个库文件对应一个.bats文件(math.bats、fileutils.bats)。test/helpers/— 将 bats-assert、bats-file、bats-support 作为 git 子模块添加。Makefile— 提供一个test目标,让贡献者只需运行make test。
使用一条命令运行完整测试套件:
# Makefile
.PHONY: test
test:
bats test/
# Run all tests recursively (Bats 1.5+)
# bats --recursive test/
# Run a specific file
# bats test/math.bats
# Run with verbose (TAP) output for CI
# bats --tap test/
# Example directory tree:
# .
# |-- lib/
# | |-- math.sh
# | `-- fileutils.sh
# |-- test/
# | |-- helpers/
# | | |-- bats-assert/
# | | `-- bats-file/
# | |-- math.bats
# | `-- fileutils.bats
# `-- Makefile知识检查:Bats-core 断言
测试您对 Bats-core 核心测试机制的理解。
回顾:使用 Bats-core 为 Bash 编写单元测试
在本课中,您学习了如何使用 Bats-core 为单个 Bash 函数组织和编写单元测试。以下是需要掌握的要点:
- 测试文件结构 — 使用
#!/usr/bin/env batsshebang,以及带有描述性名称的@test块。 load— 加载库文件,使函数可以在测试中使用,无需复制粘贴。run+$status+$output— 核心三件套;始终使用run捕获结果,以免测试立即失败。- bats-assert — 相比直接使用
[ ],优先使用assert_success、assert_failure和assert_output,以获得易读的失败信息。 setup/teardown— 在每个测试前后运行;setup_file/teardown_file每个文件只运行一次。- 模拟 — 使用通过
export -f导出的同名 shell 函数,覆盖外部命令。 skip— 有条件地跳过依赖不可用资源的测试。- 项目布局 — 将
lib/、test/和test/helpers/分开,以便维护并与 CI 集成。
这些模式可以让您的 Bash 项目遵循与任何现代软件项目相同的测试规范。
常见问题解答
「使用 Bats-core 对函数进行单元测试」课时是免费的吗?
是的 — 「使用 Bats-core 对函数进行单元测试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。
「使用 Bats-core 对函数进行单元测试」这节课中我会学到什么?
组织测试文件、断言以及 setup/teardown,以验证单个 Bash 函数。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「使用 Bats-core 对函数进行单元测试」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?
能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 Bats-core 对函数进行单元测试
- 模拟命令与替代外部工具
- 测试固件、临时环境与覆盖率
- 在 CI 管道中运行 Shell 测试