Shell 腳本的單元測試框架 文章首圖

Shell 腳本的單元測試框架

Shell 腳本的單元測試框架

在現代軟體開發中,單元測試(Unit Testing)是確保程式碼品質與穩定性的基石。然而,許多 Shell 腳本開發者往往忽略了這一環,認為 Shell 語法簡單,手動執行即可驗證。這種觀念在小型專案中或許行得通,但隨著腳本邏輯複雜度增加,缺乏自動化測試將導致維護成本飆升,甚至引發生產環境的災難。

今天我們將探討如何在 Linux 環境下建立一套輕量級且實用的 Shell 腳本單元測試框架。我們將使用最原生且無需安裝額外依賴的方式,結合 Bash 的退出碼(Exit Code)機制來實現。

為什麼需要 Shell 測試框架?

Shell 腳本通常用於自動化任務、系統管理或 CI/CD 流程。一旦腳本出錯,可能導致資料遺失或服務中斷。透過單元測試,我們可以:

  1. 即時回饋:在開發階段就發現邏輯錯誤。
  2. 回歸測試:修改程式碼後,確保原有功能未受影響。
  3. 文件化:測試案例本身即是最直觀的使用說明。

實作一個簡單的測試框架

我們將建立一個名為 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 腳本的關鍵。