Bash 测试
Shell 脚本的测试框架和模式,专注于 shunit2 和 shellspec。
框架选择
| 框架 | 优势 | 最适合 |
|---|---|---|
| shunit2 | xUnit 风格,简单,可移植 | 单元测试,函数测试 |
| shellspec | BDD 风格,现代化,功能丰富 | 行为规范,完整项目 |
shunit2
安装
# 直接下载
curl -L https://github.com/kward/shunit2/raw/master/shunit2 -o shunit2
# 或者通过包管理器
apt install shunit2
brew install shunit2
基本测试结构
#!/usr/bin/env bash
# 引入被测试的脚本
source ./my_script.sh
# 测试函数以 test 开头
test_addition() {
result=$(add 2 3)
assertEquals "2 + 3 应等于 5" "5" "$result"
}
test_string_output() {
result=$(greet "World")
assertEquals "Hello, World" "$result"
}
test_file_creation() {
create_temp_file
assertTrue "临时文件应存在" "[ -f /tmp/testfile ]"
}
test_exit_code() {
validate_input "valid"
assertEquals "有效输入应返回 0" 0 $?
}
# 设置与清理
oneTimeSetUp() {
# 在所有测试前运行一次
export TEST_DIR=$(mktemp -d)
}
oneTimeTearDown() {
# 在所有测试后运行一次
rm -rf "$TEST_DIR"
}
setUp() {
# 在每个测试前运行
cd "$TEST_DIR"
}
tearDown() {
# 在每个测试后运行
rm -f "$TEST_DIR"/*
}
# 加载 shunit2
source shunit2
shunit2 断言
# 相等性
assertEquals [消息] 期望值 实际值
assertNotEquals [消息] 不应出现的值 实际值
# 空值
assertNull [消息] 值
assertNotNull [消息] 值
# 布尔/状态
assertTrue [消息] 条件
assertFalse [消息] 条件
# 相同引用(字符串比较)
assertSame [消息] 期望值 实际值
assertNotSame [消息] 不应出现的值 实际值
# 包含(在字符串中)
assertContains [消息] 容器 内容
# 退出状态
assertEquals 0 $?
隔离测试函数
#!/usr/bin/env bash
# test_my_functions.sh
source ./my_functions.sh
test_calculate_sum_empty() {
result=$(calculate_sum)
assertEquals "空和应为 0" "0" "$result"
}
test_calculate_sum_single() {
result=$(calculate_sum 5)
assertEquals "5" "$result"
}
test_calculate_sum_multiple() {
result=$(calculate_sum 1 2 3 4 5)
assertEquals "15" "$result"
}
test_validate_email_valid() {
validate_email "[email protected]"
assertTrue "有效邮件应通过" $?
}
test_validate_email_invalid() {
validate_email "not-an-email"
assertFalse "无效邮件应失败" $?
}
source shunit2
shellspec
安装
# 通过 curl
curl -fsSL https://git.io/shellspec | sh
# 通过 Homebrew
brew install shellspec
# 通过包管理器
apt install shellspec
目录结构
project/
├── lib/
│ └── functions.sh
├── spec/
│ ├── spec_helper.sh
│ ├── functions_spec.sh
│ └── support/
│ └── fixtures/
└── .shellspec
配置 (.shellspec)
--require spec_helper
--format documentation
--color
基本规范结构
shellspec 匹配器
# 输出匹配器
The output should eq "完全匹配"
The output should include "部分匹配"
The output should start with "前缀"
The output should end with "后缀"
The output should match pattern "*通配符*"
The output should be blank
# 状态匹配器
The status should be success # 退出码 0
The status should be failure # 退出码非 0
The status should eq 1 # 特定退出码
# 变量匹配器
The variable VAR should eq "值"
The variable VAR should be defined
The variable VAR should be undefined
# 文件匹配器
The file "路径" should be exist
The file "路径" should be file
The file "路径" should be directory
The file "路径" should be readable
# 路径匹配器
The path "文件.txt" should be exist
在 shellspec 中模拟
Describe '部署函数'
# 模拟外部命令
curl() {
echo "模拟响应"
return 0
}
It '调用 API 端点'
When call deploy "server"
The output should include "模拟响应"
End
End
Describe '文件操作'
# 通过函数覆盖模拟
Mock rm
echo "rm 被调用,参数为:$*"
End
It '尝试删除文件'
When call cleanup_temp
The output should include "rm 被调用,参数为"
End
End
规范帮助器
# spec/spec_helper.sh
# 加载公共函数
spec_helper_precheck() {
minimum_version "0.28.0"
}
spec_helper_loaded() {
# 设置测试环境
export TEST_MODE=true
}
spec_helper_configure() {
# 导入项目函数
import 'lib/functions.sh'
}
测试模式
测试退出码
# shunit2
test_success_exit() {
run_command "有效输入"
assertEquals 0 $?
}
test_error_exit() {
run_command "无效输入"
assertEquals 1 $?
}
# shellspec
It '成功时退出 0'
When call run_command "有效输入"
The status should eq 0
End
测试标准输出和标准错误
# shunit2
test_stdout() {
result=$(my_function 2>/dev/null)
assertEquals "期望输出" "$result"
}
test_stderr() {
error=$(my_function 2>&1 >/dev/null)
assertContains "$error" "错误消息"
}
# shellspec
It '输出到标准输出'
When call my_function
The stdout should eq "期望输出"
End
It '将错误输出到标准错误'
When call my_function
The stderr should include "错误消息"
End
使用固定装置测试
# 设置测试固定装置
setUp() {
TEST_DIR=$(mktemp -d)
cat > "$TEST_DIR/config.json" <<EOF
{
"key": "value"
}
EOF
}
tearDown() {
rm -rf "$TEST_DIR"
}
test_config_parsing() {
result=$(parse_config "$TEST_DIR/config.json")
assertEquals "value" "$result"
}
测试交互式函数
# 通过 heredoc 提供输入
test_interactive() {
result=$(my_prompt <<EOF
yes
EOF
)
assertEquals "confirmed" "$result"
}
# 或使用 printf
test_with_input() {
result=$(printf 'yes\n' | my_prompt)
assertEquals "confirmed" "$result"
}
运行测试
# shunit2
./test_script.sh
# shellspec
shellspec # 运行所有规范
shellspec spec/file_spec.sh # 运行特定规范
shellspec --format tap # TAP 格式输出
shellspec --jobs 4 # 并行执行
最佳实践
- 实际情况下每个测试一个断言
- 描述性测试名称,说明测试内容
- 隔离测试 - 测试之间无依赖
- 测试边界情况 - 空输入、特殊字符、大数据
- 在 tearDown 中清理资源
- 模拟外部命令 - 不要测试 curl,要测试你的逻辑
- 测试退出码而不仅仅是输出