test-shell
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseShell Testing with BATS
基于BATS的Shell脚本测试
BATS (Bash Automated Testing System) for testing shell scripts and libraries.
BATS(Bash自动化测试系统)用于测试Shell脚本和函数库。
Run Commands
运行命令
bash
undefinedbash
undefinedRun all tests recursively
递归运行所有测试
bats --recursive tests/bats/
bats --recursive tests/bats/
Run specific test file
运行指定测试文件
bats tests/bats/unit/lib/test_log.bats
bats tests/bats/unit/lib/test_log.bats
Run with TAP output (for CI)
以TAP格式输出结果(适用于CI环境)
bats --tap tests/bats/
bats --tap tests/bats/
Run with timing info
运行测试并显示耗时信息
bats --timing tests/bats/
bats --timing tests/bats/
Filter tests by name pattern
按名称模式筛选测试用例
bats --filter "validate_semver" tests/bats/
bats --filter "validate_semver" tests/bats/
Run in parallel
并行运行测试
bats --jobs 4 tests/bats/
undefinedbats --jobs 4 tests/bats/
undefinedDirectory Structure
目录结构
text
tests/
├── bats/
│ ├── unit/
│ │ └── lib/
│ │ ├── test_log.bats
│ │ ├── test_fs.bats
│ │ ├── github/
│ │ │ └── test_output.bats
│ │ └── release/
│ │ └── test_version.bats
│ └── integration/
│ └── test_actions.bats
├── helpers/
│ ├── common.bash # Shared utilities
│ ├── mocks.bash # Command mocking
│ └── github_env.bash # GitHub Actions simulation
└── fixtures/
└── json/ # Test data filestext
tests/
├── bats/
│ ├── unit/
│ │ └── lib/
│ │ ├── test_log.bats
│ │ ├── test_fs.bats
│ │ ├── github/
│ │ │ └── test_output.bats
│ │ └── release/
│ │ └── test_version.bats
│ └── integration/
│ └── test_actions.bats
├── helpers/
│ ├── common.bash # 共享工具函数
│ ├── mocks.bash # 命令模拟工具
│ └── github_env.bash # GitHub Actions环境模拟
└── fixtures/
└── json/ # 测试数据文件Test File Template
测试文件模板
bash
#!/usr/bin/env batsbash
#!/usr/bin/env batsSPDX-License-Identifier: MIT
SPDX-License-Identifier: MIT
Purpose: Tests for scripts/ci/lib/example.sh
用途:测试scripts/ci/lib/example.sh脚本
load "../helpers/common"
load "../helpers/mocks"
setup() {
setup_temp_dir
export LIB_DIR
}
teardown() {
teardown_temp_dir
}
load "../helpers/common"
load "../helpers/mocks"
setup() {
setup_temp_dir
export LIB_DIR
}
teardown() {
teardown_temp_dir
}
=============================================================================
=============================================================================
Function group description
函数组描述
=============================================================================
=============================================================================
@test "function_name: does expected thing" {
run bash -c 'source "$LIB_DIR/example.sh" && function_name "arg"'
assert_success
assert_output "expected output"
}
@test "function_name: handles edge case" {
run bash -c 'source "$LIB_DIR/example.sh" && function_name ""'
assert_failure
assert_output --partial "error message"
}
undefined@test "function_name: 实现预期功能" {
run bash -c 'source "$LIB_DIR/example.sh" && function_name "arg"'
assert_success
assert_output "预期输出"
}
@test "function_name: 处理边界情况" {
run bash -c 'source "$LIB_DIR/example.sh" && function_name ""'
assert_failure
assert_output --partial "错误提示"
}
undefinedAssertions (bats-assert)
断言(bats-assert)
bash
undefinedbash
undefinedExit code assertions
退出码断言
assert_success # Exit code 0
assert_failure # Exit code != 0
assert_success # 断言退出码为0
assert_failure # 断言退出码不为0
Output assertions
输出断言
assert_output "exact match"
assert_output --partial "substring"
assert_output --regexp "pattern.*here"
refute_output # Output is empty
refute_output --partial "not present"
assert_output "精确匹配内容"
assert_output --partial "包含子字符串"
assert_output --regexp "正则模式.*匹配"
refute_output # 断言输出为空
refute_output --partial "不包含指定内容"
Line assertions (for multiline output)
行断言(针对多行输出)
assert_line "exact line"
assert_line --partial "substring in any line"
assert_line --index 0 "first line"
assert_line "精确匹配行内容"
assert_line --partial "任意行包含子字符串"
assert_line --index 0 "第一行内容"
Equality
相等性断言
assert_equal "$expected" "$actual"
undefinedassert_equal "$expected" "$actual"
undefinedFile Assertions (bats-file)
文件断言(bats-file)
bash
assert_file_exists "/path/to/file"
assert_file_not_exists "/path/to/file"
assert_dir_exists "/path/to/dir"
assert_file_contains "/path/to/file" "expected content"bash
assert_file_exists "/path/to/file"
assert_file_not_exists "/path/to/file"
assert_dir_exists "/path/to/dir"
assert_file_contains "/path/to/file" "预期文件内容"Setup and Teardown
初始化与清理操作
bash
undefinedbash
undefinedPer-test setup/teardown
每个测试用例的初始化/清理
setup() {
setup_temp_dir # Create $BATS_TEST_TMPDIR
export LIB_DIR # Make available to subshells
}
teardown() {
teardown_temp_dir # Clean up temp files
}
setup() {
setup_temp_dir # 创建临时目录$BATS_TEST_TMPDIR
export LIB_DIR # 让子shell可以访问该变量
}
teardown() {
teardown_temp_dir # 清理临时文件
}
Per-file setup/teardown (run once)
每个测试文件的初始化/清理(仅运行一次)
setup_file() {
# Heavy one-time setup (e.g., start services)
}
teardown_file() {
# One-time cleanup
}
undefinedsetup_file() {
# 耗时较长的一次性初始化操作(如启动服务)
}
teardown_file() {
# 一次性清理操作
}
undefinedMocking Commands
命令模拟
bash
undefinedbash
undefinedIn mocks.bash helper:
在mocks.bash辅助工具中:
mock_command() {
local cmd="$1"
local output="$2"
local exit_code="${3:-0}"
local mock_dir="${BATS_TEST_TMPDIR}/mocks"
mkdir -p "$mock_dir"
cat > "${mock_dir}/${cmd}" <<MOCK#!/usr/bin/env bash
echo "$output"
exit $exit_code
MOCK
chmod +x "${mock_dir}/${cmd}"
export PATH="${mock_dir}:$PATH"
}
mock_command() {
local cmd="$1"
local output="$2"
local exit_code="${3:-0}"
local mock_dir="${BATS_TEST_TMPDIR}/mocks"
mkdir -p "$mock_dir"
cat > "${mock_dir}/${cmd}" <<MOCK#!/usr/bin/env bash
echo "$output"
exit $exit_code
MOCK
chmod +x "${mock_dir}/${cmd}"
export PATH="${mock_dir}:$PATH"
}
Usage in test:
在测试用例中使用:
@test "handles curl failure" {
mock_command "curl" "Connection refused" 1
run my_download_function "http://example.com"
assert_failure
}
undefined@test "处理curl请求失败场景" {
mock_command "curl" "Connection refused" 1
run my_download_function "http://example.com"
assert_failure
}
undefinedTesting with Bash 4+ Features
针对Bash 4+特性的测试
macOS ships with Bash 3.2. For tests requiring Bash 4+ features:
bash
undefinedmacOS默认预装Bash 3.2。对于需要Bash 4+特性的测试:
bash
undefinedIn common.bash:
在common.bash中:
bash4_available() {
[[ "${BASH4_AVAILABLE:-0}" -eq 1 ]]
}
bash4_available() {
[[ "${BASH4_AVAILABLE:-0}" -eq 1 ]]
}
In tests:
在测试用例中:
@test "feature requiring bash 4+" {
if ! bash4_available; then
skip "requires bash 4+"
fi
# ... test code using ${var,,} or associative arrays
}
undefined@test "需要Bash 4+的功能" {
if ! bash4_available; then
skip "需要Bash 4+版本"
fi
# ... 使用${var,,}或关联数组的测试代码
}
undefinedGitHub Actions Environment Simulation
GitHub Actions环境模拟
bash
undefinedbash
undefinedIn github_env.bash helper:
在github_env.bash辅助工具中:
setup_github_env() {
export GITHUB_OUTPUT="${BATS_TEST_TMPDIR}/github_output"
export GITHUB_ENV="${BATS_TEST_TMPDIR}/github_env"
export GITHUB_STEP_SUMMARY="${BATS_TEST_TMPDIR}/step_summary"
touch "$GITHUB_OUTPUT" "$GITHUB_ENV" "$GITHUB_STEP_SUMMARY"
}
setup_github_env() {
export GITHUB_OUTPUT="${BATS_TEST_TMPDIR}/github_output"
export GITHUB_ENV="${BATS_TEST_TMPDIR}/github_env"
export GITHUB_STEP_SUMMARY="${BATS_TEST_TMPDIR}/step_summary"
touch "$GITHUB_OUTPUT" "$GITHUB_ENV" "$GITHUB_STEP_SUMMARY"
}
Assert GitHub output was set:
断言GitHub输出已设置:
get_github_output() {
local key="$1"
grep "^${key}=" "$GITHUB_OUTPUT" | cut -d= -f2-
}
@test "sets github output" {
setup_github_env
run bash -c 'source "$LIB_DIR/github/output.sh" && set_github_output "key" "value"'
assert_success
assert_equal "value" "$(get_github_output "key")"
}
undefinedget_github_output() {
local key="$1"
grep "^${key}=" "$GITHUB_OUTPUT" | cut -d= -f2-
}
@test "设置GitHub输出参数" {
setup_github_env
run bash -c 'source "$LIB_DIR/github/output.sh" && set_github_output "key" "value"'
assert_success
assert_equal "value" "$(get_github_output "key")"
}
undefinedCoverage with kcov
基于kcov的代码覆盖率统计
bash
undefinedbash
undefinedRun with coverage
运行测试并生成覆盖率报告
kcov --include-path=scripts/ci/lib coverage-report bats tests/bats/
kcov --include-path=scripts/ci/lib coverage-report bats tests/bats/
CI workflow configuration
CI工作流配置
coverage: true
coverage-threshold: 80
upload-coverage: true
undefinedcoverage: true
coverage-threshold: 80
upload-coverage: true
undefinedBest Practices
最佳实践
- One assertion focus per test - Test one behavior, use descriptive names
- Use setup/teardown - Never leave temp files behind
- Isolate tests - Each test should work independently
- Mock external commands - Don't depend on network/external state
- Group related tests - Use comment headers to organize
- Test edge cases - Empty strings, missing files, invalid input
- Keep tests fast - Avoid sleep, minimize I/O
- 每个测试聚焦单一断言 - 测试单一行为,使用描述性名称
- 使用初始化/清理操作 - 绝不遗留临时文件
- 测试用例隔离 - 每个测试应独立运行
- 模拟外部命令 - 不依赖网络或外部状态
- 相关测试分组 - 使用注释标题组织测试用例
- 测试边界情况 - 空字符串、缺失文件、无效输入等场景
- 保持测试高效 - 避免使用sleep,尽量减少IO操作
Common Patterns
常见模式
Testing Exit Codes
测试退出码
bash
@test "die: exits with code 1" {
run bash -c 'source "$LIB_DIR/log.sh" && die "error"'
assert_failure
assert_equal "1" "$status"
}bash
@test "die函数:以退出码1终止" {
run bash -c 'source "$LIB_DIR/log.sh" && die "error"'
assert_failure
assert_equal "1" "$status"
}Testing stderr vs stdout
测试stderr与stdout
bash
@test "logs to stderr" {
run bash -c 'source "$LIB_DIR/log.sh" && log_error "msg" 2>&1'
assert_output --partial "ERROR"
}bash
@test "错误日志输出到stderr" {
run bash -c 'source "$LIB_DIR/log.sh" && log_error "msg" 2>&1'
assert_output --partial "ERROR"
}Testing Function Exports
测试函数导出
bash
@test "function is exported" {
source "$LIB_DIR/example.sh"
run bash -c "declare -F my_function"
assert_success
}bash
@test "函数已导出" {
source "$LIB_DIR/example.sh"
run bash -c "declare -F my_function"
assert_success
}Skipping Tests
跳过测试用例
bash
@test "requires specific tool" {
command -v special_tool >/dev/null || skip "special_tool not installed"
# ... test code
}bash
@test "依赖特定工具的测试" {
command -v special_tool >/dev/null || skip "未安装special_tool"
# ... 测试代码
}