Shell 腳本的單元測試框架
在現代軟體開發中,單元測試(Unit Testing)是確保程式碼品質與穩定性的基石。然而,許多 Shell 腳本開發者往往忽略了這一環,認為 Shell 語法簡單,手動執行即可驗證。這種觀念在小型專案中或許行得通,但隨著腳本邏輯複雜度增加,缺乏自動化測試將導致維護成本飆升,甚至引發生產環境的災難。
今天我們將探討如何在 Linux 環境下建立一套輕量級且實用的 Shell 腳本單元測試框架。我們將使用最原生且無需安裝額外依賴的方式,結合 Bash 的退出碼(Exit Code)機制來實現。
為什麼需要 Shell 測試框架?
Shell 腳本通常用於自動化任務、系統管理或 CI/CD 流程。一旦腳本出錯,可能導致資料遺失或服務中斷。透過單元測試,我們可以:
- 即時回饋:在開發階段就發現邏輯錯誤。
- 回歸測試:修改程式碼後,確保原有功能未受影響。
- 文件化:測試案例本身即是最直觀的使用說明。
實作一個簡單的測試框架
我們將建立一個名為 shell-test 的微型框架。核心概念非常簡單:每個測試函式執行完畢後,若結果符合預期則返回 0(成功),否則返回非 0(失敗)。我們撰寫一個主循環來統計成功與失敗的數量。
首先,建立專案目錄結構:
mkdir shell-test-demo
cd shell-test-demo
touch test_helper.sh
touch test_math.sh
touch run_tests.sh
1. 測試輔助函式
在 test_helper.sh 中,我們定義斷言(Assert)函式。這裡我們使用最基礎的 assert_equal,它會比較兩個值是否相等。
#!/bin/bash
# 全局變數用於統計
TEST_PASSED=0
TEST_FAILED=0
TEST_TOTAL=0
# 斷言函式:檢查實際值是否等於預期值
assert_equal() {
local expected="$1"
local actual="$2"
local msg="${3:-}"
TEST_TOTAL=$((TEST_TOTAL + 1))
if [[ "$expected" == "$actual" ]]; then
echo -e "\033[32m[PASS]\033[0m Test $TEST_TOTAL: $msg"
TEST_PASSED=$((TEST_PASSED + 1))
return 0
else
echo -e "\033[31m[FAIL]\033[0m Test $TEST_TOTAL: $msg"
echo " Expected: $expected"
echo " Actual: $actual"
TEST_FAILED=$((TEST_FAILED + 1))
return 1
fi
}
2. 被測試的腳本與測試案例
假設我們有一個簡單的數學工具腳本 math_utils.sh,我們需要測試它。
# math_utils.sh
add() {
echo $(($1 + $2))
}
接著,在 test_math.sh 中撰寫測試案例:
#!/bin/bash
# 引入輔助函式
source "$(dirname "$0")/test_helper.sh"
# 引入被測試的腳本
source "$(dirname "$0")/math_utils.sh"
test_add_positive_numbers() {
local result
result=$(add 2 3)
assert_equal "5" "$result" "Addition of 2 and 3 should be 5"
}
test_add_negative_numbers() {
local result
result=$(add -2 -3)
assert_equal "-5" "$result" "Addition of -2 and -3 should be -5"
}
# 執行所有測試案例
test_add_positive_numbers
test_add_negative_numbers
3. 測試運行器
最後,run_tests.sh 負責匯入所有測試檔案並顯示總結。
#!/bin/bash
echo "Starting Shell Unit Tests..."
echo "================================"
# 匯入測試案例(這裡手動指定,也可改為自動掃描)
source "$(dirname "$0")/test_math.sh"
echo "================================"
echo "Test Summary:"
echo "Total: $TEST_TOTAL"
echo -e "Passed: \033[32m$TEST_PASSED\033[0m"
echo -e "Failed: \033[31m$TEST_FAILED\033[0m"
# 若有失敗,回傳非零狀態碼,適合用於 CI/CD
if [[ $TEST_FAILED -gt 0 ]]; then
exit 1
else
exit 0
fi
執行測試
確保所有檔案都有執行權限,然後運行:
chmod +x run_tests.sh test_math.sh test_helper.sh math_utils.sh
./run_tests.sh
你應該會看到類似以下的輸出:
Starting Shell Unit Tests...
================================
[PASS] Test 1: Addition of 2 and 3 should be 5
[PASS] Test 2: Addition of -2 and -3 should be -5
================================
Test Summary:
Total: 2
Passed: 2
Failed: 0
常見問題
Q1: 如何測試命令列引數或環境變數?
Shell 腳本常依賴環境變數或 $1 等引數。在測試時,你可以直接在測試案例中設定環境變數,例如 export DB_HOST=localhost,或修改 math_utils.sh 使其接受引數,並在測試中傳入特定值。記得在測試前後清除環境,避免污染。
Q2: 測試失敗時如何除錯?
上述框架在 [FAIL] 時會顯示預期值與實際值。若測試邏輯複雜,建議在測試案例中加入 set -x 來開啟除錯模式,或將 assert_equal 改為更強大的 assert_output 來比對完整輸出內容。此外,確保測試案例之間是獨立的,不要依賴前一個測試建立的檔案或狀態。
小結
透過上述簡單的架構,我們已經建立了一個可擴展的 Shell 單元測試基礎。雖然這不如 Bats 或 shunit2 等第三方框架功能強大,但它零依賴、易於理解,非常適合小型專案或作為學習測試概念的起點。當專案規模擴大時,你可以輕鬆地將核心邏輯重構至 shunit2,而測試案例的邏輯幾乎可以無縫轉移。記住,好的測試習慣是寫出健壯 Shell 腳本的關鍵。