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 "有效 email 應通過" $?
}
test_validate_email_invalid() {
validate_email "not-an-email"
assertFalse "無效 email 應失敗" $?
}
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 "*glob*"
The output should be blank
# 狀態匹配器
The status should be success # 離開碼 0
The status should be failure # 離開碼非零
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 "file.txt" should be exist
在 shellspec 中模擬
Describe 'deploy 函式'
# 模擬外部指令
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 "valid_input"
assertEquals 0 $?
}
test_error_exit() {
run_command "invalid_input"
assertEquals 1 $?
}
# shellspec
It '成功時離開碼為 0'
When call run_command "valid_input"
The status should eq 0
End
測試 stdout 與 stderr
# shunit2
test_stdout() {
result=$(my_function 2>/dev/null)
assertEquals "預期輸出" "$result"
}
test_stderr() {
error=$(my_function 2>&1 >/dev/null)
assertContains "$error" "錯誤訊息"
}
# shellspec
It '輸出至 stdout'
When call my_function
The stdout should eq "預期輸出"
End
It '將錯誤輸出至 stderr'
When call my_function
The stderr should include "錯誤訊息"
End
使用 Fixtures 測試
# 設定測試 Fixtures
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,測試你的邏輯
- 測試離開碼而不僅僅是輸出