bats-testing-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBats Testing Patterns
Bats测试模式
Comprehensive guidance for writing comprehensive unit tests for shell scripts using Bats (Bash Automated Testing System), including test patterns, fixtures, and best practices for production-grade shell testing.
本文提供了使用Bats(Bash自动化测试系统)编写全面Shell脚本单元测试的综合指南,包括测试模式、测试夹具以及生产级Shell测试的最佳实践。
When to Use This Skill
何时使用该技能
- Writing unit tests for shell scripts
- Implementing test-driven development (TDD) for scripts
- Setting up automated testing in CI/CD pipelines
- Testing edge cases and error conditions
- Validating behavior across different shell environments
- Building maintainable test suites for scripts
- Creating fixtures for complex test scenarios
- Testing multiple shell dialects (bash, sh, dash)
- 为Shell脚本编写单元测试
- 为脚本实现测试驱动开发(TDD)
- 在CI/CD流水线中设置自动化测试
- 测试边缘情况和错误条件
- 在不同Shell环境中验证行为
- 为脚本构建可维护的测试套件
- 为复杂测试场景创建测试夹具
- 测试多种Shell方言(bash、sh、dash)
Bats Fundamentals
Bats基础
What is Bats?
什么是Bats?
Bats (Bash Automated Testing System) is a TAP (Test Anything Protocol) compliant testing framework for shell scripts that provides:
- Simple, natural test syntax
- TAP output format compatible with CI systems
- Fixtures and setup/teardown support
- Assertion helpers
- Parallel test execution
Bats(Bash自动化测试系统)是一个兼容TAP(测试任何协议)的Shell脚本测试框架,提供以下功能:
- 简洁、自然的测试语法
- 与CI系统兼容的TAP输出格式
- 测试夹具和初始化/清理支持
- 断言辅助工具
- 并行测试执行
Installation
安装
bash
undefinedbash
undefinedmacOS with Homebrew
macOS使用Homebrew
brew install bats-core
brew install bats-core
Ubuntu/Debian
Ubuntu/Debian
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local
From npm (Node.js)
从npm(Node.js)安装
npm install --global bats
npm install --global bats
Verify installation
验证安装
bats --version
undefinedbats --version
undefinedFile Structure
文件结构
project/
├── bin/
│ ├── script.sh
│ └── helper.sh
├── tests/
│ ├── test_script.bats
│ ├── test_helper.sh
│ ├── fixtures/
│ │ ├── input.txt
│ │ └── expected_output.txt
│ └── helpers/
│ └── mocks.bash
└── README.mdproject/
├── bin/
│ ├── script.sh
│ └── helper.sh
├── tests/
│ ├── test_script.bats
│ ├── test_helper.sh
│ ├── fixtures/
│ │ ├── input.txt
│ │ └── expected_output.txt
│ └── helpers/
│ └── mocks.bash
└── README.mdBasic Test Structure
基础测试结构
Simple Test File
简单测试文件
bash
#!/usr/bin/env batsbash
#!/usr/bin/env batsLoad test helper if present
加载测试辅助工具(如果存在)
load test_helper
load test_helper
Setup runs before each test
Setup会在每个测试前运行
setup() {
export TMPDIR=$(mktemp -d)
}
setup() {
export TMPDIR=$(mktemp -d)
}
Teardown runs after each test
Teardown会在每个测试后运行
teardown() {
rm -rf "$TMPDIR"
}
teardown() {
rm -rf "$TMPDIR"
}
Test: simple assertion
测试:简单断言
@test "Function returns 0 on success" {
run my_function "input"
[ "$status" -eq 0 ]
}
@test "函数执行成功时返回0" {
run my_function "input"
[ "$status" -eq 0 ]
}
Test: output verification
测试:输出验证
@test "Function outputs correct result" {
run my_function "test"
[ "$output" = "expected output" ]
}
@test "函数输出正确结果" {
run my_function "test"
[ "$output" = "expected output" ]
}
Test: error handling
测试:错误处理
@test "Function returns 1 on missing argument" {
run my_function
[ "$status" -eq 1 ]
}
undefined@test "函数在缺少参数时返回1" {
run my_function
[ "$status" -eq 1 ]
}
undefinedAssertion Patterns
断言模式
Exit Code Assertions
退出码断言
bash
#!/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" ]
}bash
#!/usr/bin/env bats
@test "命令执行成功" {
run true
[ "$status" -eq 0 ]
}
@test "命令按预期执行失败" {
run false
[ "$status" -ne 0 ]
}
@test "命令返回特定退出码" {
run my_function --invalid
[ "$status" -eq 127 ]
}
@test "可以捕获命令结果" {
run echo "hello"
[ $status -eq 0 ]
[ "$output" = "hello" ]
}Output Assertions
输出断言
bash
#!/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" ]
}bash
#!/usr/bin/env bats
@test "输出匹配指定字符串" {
result=$(echo "hello world")
[ "$result" = "hello world" ]
}
@test "输出包含子字符串" {
result=$(echo "hello world")
[[ "$result" == *"world"* ]]
}
@test "输出匹配指定模式" {
result=$(date +%Y)
[[ "$result" =~ ^[0-9]{4}$ ]]
}
@test "多行输出" {
run printf "line1\nline2\nline3"
[ "$output" = "line1
line2
line3" ]
}
@test "Lines变量包含输出内容" {
run printf "line1\nline2\nline3"
[ "${lines[0]}" = "line1" ]
[ "${lines[1]}" = "line2" ]
[ "${lines[2]}" = "line3" ]
}File Assertions
文件断言
bash
#!/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 ]
}bash
#!/usr/bin/env bats
@test "文件已创建" {
[ ! -f "$TMPDIR/output.txt" ]
my_function > "$TMPDIR/output.txt"
[ -f "$TMPDIR/output.txt" ]
}
@test "文件内容与预期匹配" {
my_function > "$TMPDIR/output.txt"
[ "$(cat "$TMPDIR/output.txt")" = "expected content" ]
}
@test "文件可读" {
touch "$TMPDIR/test.txt"
[ -r "$TMPDIR/test.txt" ]
}
@test "文件权限正确" {
touch "$TMPDIR/test.txt"
chmod 644 "$TMPDIR/test.txt"
[ "$(stat -f %OLp "$TMPDIR/test.txt")" = "644" ]
}
@test "文件大小正确" {
echo -n "12345" > "$TMPDIR/test.txt"
[ "$(wc -c < "$TMPDIR/test.txt")" -eq 5 ]
}Setup and Teardown Patterns
初始化和清理模式
Basic Setup and Teardown
基础初始化和清理
bash
#!/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" ]
}bash
#!/usr/bin/env bats
setup() {
# 创建测试目录
TEST_DIR=$(mktemp -d)
export TEST_DIR
# 引入待测试的脚本
source "${BATS_TEST_DIRNAME}/../bin/script.sh"
}
teardown() {
# 清理临时目录
rm -rf "$TEST_DIR"
}
@test "使用TEST_DIR进行测试" {
touch "$TEST_DIR/file.txt"
[ -f "$TEST_DIR/file.txt" ]
}Setup with Resources
带资源的初始化
bash
#!/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" ]
}bash
#!/usr/bin/env bats
setup() {
# 创建目录结构
mkdir -p "$TMPDIR/data/input"
mkdir -p "$TMPDIR/data/output"
# 创建测试夹具
echo "line1" > "$TMPDIR/data/input/file1.txt"
echo "line2" > "$TMPDIR/data/input/file2.txt"
# 初始化环境变量
export DATA_DIR="$TMPDIR/data"
export INPUT_DIR="$DATA_DIR/input"
export OUTPUT_DIR="$DATA_DIR/output"
}
teardown() {
rm -rf "$TMPDIR/data"
}
@test "处理输入文件" {
run my_process_script "$INPUT_DIR" "$OUTPUT_DIR"
[ "$status" -eq 0 ]
[ -f "$OUTPUT_DIR/file1.txt" ]
}Global Setup/Teardown
全局初始化/清理
bash
#!/usr/bin/env batsbash
#!/usr/bin/env batsLoad shared setup from test_helper.sh
从test_helper.sh加载共享初始化配置
load test_helper
load test_helper
setup_file runs once before all tests
setup_file会在所有测试前运行一次
setup_file() {
export SHARED_RESOURCE=$(mktemp -d)
echo "Expensive setup" > "$SHARED_RESOURCE/data.txt"
}
setup_file() {
export SHARED_RESOURCE=$(mktemp -d)
echo "Expensive setup" > "$SHARED_RESOURCE/data.txt"
}
teardown_file runs once after all tests
teardown_file会在所有测试后运行一次
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" ]
}
undefinedteardown_file() {
rm -rf "$SHARED_RESOURCE"
}
@test "第一个测试使用共享资源" {
[ -f "$SHARED_RESOURCE/data.txt" ]
}
@test "第二个测试使用共享资源" {
[ -d "$SHARED_RESOURCE" ]
}
undefinedMocking and Stubbing Patterns
模拟和存根模式
Function Mocking
函数模拟
bash
#!/usr/bin/env batsbash
#!/usr/bin/env batsMock 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" ]]
}
undefinedmy_external_tool() {
echo "mocked output"
return 0
}
@test "函数使用模拟工具" {
export -f my_external_tool
run my_function
[[ "$output" == "mocked output" ]]
}
undefinedCommand Stubbing
命令存根
bash
#!/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 ]
}bash
#!/usr/bin/env bats
setup() {
# 创建存根目录
STUBS_DIR="$TMPDIR/stubs"
mkdir -p "$STUBS_DIR"
# 添加到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 "函数配合存根的curl使用" {
create_stub curl "{ \"status\": \"ok\" }" 0
run my_api_function
[ "$status" -eq 0 ]
}Variable Stubbing
变量存根
bash
#!/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"* ]]
}bash
#!/usr/bin/env bats
@test "函数处理环境变量覆盖" {
export MY_SETTING="override_value"
run my_function
[ "$status" -eq 0 ]
[[ "$output" == *"override_value"* ]]
}
@test "变量未设置时函数使用默认值" {
unset MY_SETTING
run my_function
[ "$status" -eq 0 ]
[[ "$output" == *"default"* ]]
}Fixture Management
测试夹具管理
Using Fixture Files
使用夹具文件
bash
#!/usr/bin/env batsbash
#!/usr/bin/env batsFixture directory: tests/fixtures/
夹具目录: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"}
undefinedsetup() {
FIXTURES_DIR="${BATS_TEST_DIRNAME}/fixtures"
WORK_DIR=$(mktemp -d)
export WORK_DIR
}
teardown() {
rm -rf "$WORK_DIR"
}
@test "处理夹具文件" {
# 将夹具复制到工作目录
cp "$FIXTURES_DIR/input.txt" "$WORK_DIR/input.txt"
# 运行函数
run my_process_function "$WORK_DIR/input.txt"
# 比较输出
diff "$WORK_DIR/output.txt" "$FIXTURES_DIR/expected_output.txt"}
undefinedDynamic Fixture Generation
动态生成夹具
bash
#!/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 ]
}bash
#!/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 "处理大型输入文件" {
generate_fixture 1000 "$TMPDIR/large.txt"
run my_function "$TMPDIR/large.txt"
[ "$status" -eq 0 ]
[ "$(wc -l < "$TMPDIR/large.txt")" -eq 1000 ]
}Advanced Patterns
高级模式
Testing Error Conditions
测试错误条件
bash
#!/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:"* ]]
}bash
#!/usr/bin/env bats
@test "函数在文件不存在时执行失败" {
run my_function "/nonexistent/file.txt"
[ "$status" -ne 0 ]
[[ "$output" == *"not found"* ]]
}
@test "函数在输入为空时执行失败" {
run my_function ""
[ "$status" -ne 0 ]
}
@test "函数在权限不足时执行失败" {
touch "$TMPDIR/readonly.txt"
chmod 000 "$TMPDIR/readonly.txt"
run my_function "$TMPDIR/readonly.txt"
[ "$status" -ne 0 ]
chmod 644 "$TMPDIR/readonly.txt" # 清理
}
@test "函数提供有用的错误信息" {
run my_function --invalid-option
[ "$status" -ne 0 ]
[[ "$output" == *"Usage:"* ]]
}Testing with Dependencies
带依赖的测试
bash
#!/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 ]
}bash
#!/usr/bin/env bats
setup() {
# 检查是否存在所需工具
if ! command -v jq &>/dev/null; then
skip "jq未安装"
fi
export SCRIPT="${BATS_TEST_DIRNAME}/../bin/script.sh"
}
@test "JSON解析功能正常" {
skip_if ! command -v jq &>/dev/null
run my_json_parser '{"key": "value"}'
[ "$status" -eq 0 ]
}Testing Shell Compatibility
测试Shell兼容性
bash
#!/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
#!/usr/bin/env bats
@test "脚本在bash中正常运行" {
bash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}
@test "脚本在sh(POSIX)中正常运行" {
sh "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
}
@test "脚本在dash中正常运行" {
if command -v dash &>/dev/null; then
dash "${BATS_TEST_DIRNAME}/../bin/script.sh" arg1
else
skip "dash未安装"
fi
}Parallel Execution
并行执行
bash
#!/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" ]
}bash
#!/usr/bin/env bats
@test "多个独立操作" {
run bash -c 'for i in {1..10}; do
my_operation "$i" &
done
wait'
[ "$status" -eq 0 ]
}
@test "并发文件操作" {
for i in {1..5}; do
my_function "$TMPDIR/file$i" &
done
wait
[ -f "$TMPDIR/file1" ]
[ -f "$TMPDIR/file5" ]
}Test Helper Pattern
测试辅助工具模式
test_helper.sh
test_helper.sh
bash
#!/usr/bin/env bashbash
#!/usr/bin/env bashSource script under test
引入待测试的脚本
export SCRIPT_DIR="${BATS_TEST_DIRNAME%/*}/bin"
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}
assert_file_exists() {
if [ ! -f "$1" ]; then
echo "预期文件存在:$1"
return 1
fi
}
assert_file_equals() {
local file="$1"
local expected="$2"
if [ ! -f "$file" ]; then
echo "文件不存在:$file"
return 1
fi
local actual=$(cat "$file")
if [ "$actual" != "$expected" ]; then
echo "文件内容不匹配"
echo "预期:$expected"
echo "实际:$actual"
return 1
fi}
Create temporary test directory
创建临时测试目录
setup_test_dir() {
export TEST_DIR=$(mktemp -d)
}
cleanup_test_dir() {
rm -rf "$TEST_DIR"
}
undefinedsetup_test_dir() {
export TEST_DIR=$(mktemp -d)
}
Integration with CI/CD
清理临时测试目录
GitHub Actions Workflow
—
yaml
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.tapcleanup_test_dir() {
rm -rf "$TEST_DIR"
}
undefinedMakefile Integration
与CI/CD集成
—
GitHub Actions工作流
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 reportsyaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 安装Bats
run: |
npm install --global bats
- name: 运行测试
run: |
bats tests/*.bats
- name: 使用Tap报告器运行测试
run: |
bats tests/*.bats --tap | tee test_output.tapBest Practices
Makefile集成
- Test one thing per test - Single responsibility principle
- Use descriptive test names - Clearly states what is being tested
- Clean up after tests - Always remove temporary files in teardown
- Test both success and failure paths - Don't just test happy path
- Mock external dependencies - Isolate unit under test
- Use fixtures for complex data - Makes tests more readable
- Run tests in CI/CD - Catch regressions early
- Test across shell dialects - Ensure portability
- Keep tests fast - Run in parallel when possible
- Document complex test setup - Explain unusual patterns
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
# 可选:生成覆盖率报告Resources
最佳实践
- Bats GitHub: https://github.com/bats-core/bats-core
- Bats Documentation: https://bats-core.readthedocs.io/
- TAP Protocol: https://testanything.org/
- Test-Driven Development: https://en.wikipedia.org/wiki/Test-driven_development
- 每个测试只测试一件事 - 单一职责原则
- 使用描述性的测试名称 - 清晰说明测试内容
- 测试后清理 - 始终在teardown中删除临时文件
- 同时测试成功和失败路径 - 不要只测试正常路径
- 模拟外部依赖 - 隔离待测试单元
- 使用夹具处理复杂数据 - 让测试更易读
- 在CI/CD中运行测试 - 尽早发现回归问题
- 跨Shell方言测试 - 确保可移植性
- 保持测试快速 - 尽可能并行运行
- 记录复杂的测试设置 - 解释不常见的模式
—