Zsh 開發指南(第二十一篇 測試方法以及編寫可測試代碼的方法)

導讀

在正式的場景,代碼寫完後都是須要測試的,shell 腳本也不例外。但 shell 腳本的特性致使測試方法和其餘語言有所不一樣。git

單元測試

做爲一種重要的測試方法,單元測試在不少種編程語言程序測試中起到舉重輕重的做用。但不幸的是,單元測試基本不適用於 shell 腳本。並非說 shell 腳本不能被單元測試,而是說單元測試能測試出來的問題不多,投入卻很大。爲了讓 shell 腳本能被單元測試,50 行的代碼極可能要改寫成 100 多行甚至更多行。更重要的是 shell 腳本嚴重依賴外部環境,多數問題須要對腳本總體進行功能測試才能發現,而不是對單個函數進行單元測試。對單元測試的精力投入極可能會減小在功能測試的精力投入。github

因此不建議推行 shell 腳本的單元測試,這不只會讓開發者很痛苦,也很難減小問題的出現概率,甚至有可能拔苗助長。shell

單個腳本的功能測試

Shell 腳本的最小測試粒度是單個腳本。必須保證單個腳本是容易測試的,不能多個腳本耦合太緊密而難以對其中某一個進行單獨測試。數據庫

有主體邏輯的腳本依賴的外部環境必須是容易模擬的。好比須要從數據庫中讀取數據,對數據進行處理,而後寫入到文件中,這些功能不能在同一個腳本中完成。由於數據庫這個外部環境不容易模擬,會致使測試困難。須要把讀寫數據庫的功能獨立成單獨的腳本,功能儘可能簡單,測試該腳本時只須要關心數據是否正常讀取了出來,格式是否被正確轉換等等,而不須要關心處理數據的具體邏輯。處理數據的主體邏輯代碼要獨立成一個(或者多個)腳本,測試該腳本時,無需準備數據庫環境,直接用另外一個腳本或者數據文件取代讀取數據庫的腳本,提供測試數據。若是文件寫入的環境複雜(好比文件或者目錄結構複雜,或者要寫入到分佈式文件系統等等),也須要將文件寫入的腳本獨立出來以便更易於測試。編程

對有主體邏輯的腳本進行功能測試,不能手動進行,必須寫測試腳本,能夠自動運行。每次腳本改動後進行迴歸測試。項目穩定後,能夠在每次提交代碼後自動運行測試腳本。測試腳本必須覆蓋正常和異常狀況,不能只覆蓋正常狀況。異常狀況的多少,要根據腳本的複雜度而定。微信

有複雜外部依賴的腳本,功能必須單一,邏輯儘可能簡單,代碼儘可能穩定,不常常改動。好比讀寫數據庫、啓停進程、複雜的目錄文件操做等有複雜外部依賴的腳本,功能必須單一,只與一個特定的外部依賴交互,提供儘可能和外部依賴無關的中間數據,儘可能不包含和外部環境無關的邏輯。該類腳本要容易模擬,以便在測試其餘部分時再也不須要依賴外部環境。編程語言

對於有複雜外部依賴的腳本,能夠寫腳本自動測試,也能夠手動測試,測試時須要包含正常和異常的狀況,不能只測試正常狀況。分佈式

功能測試示例

須要寫腳本完成以下功能:ide

若是 process1 和 process2 兩個進程都存在,以 process2 進程 cwd 目錄中的 data/output.txt 爲輸入,作一些比較複雜的處理,而後輸出到 process1 進程 cwd 目錄中的 data/input.txt 文件(若是該文件已存在,則不處理),處理完後,刪除以前的 data/output.txt函數

分析:

process1 和 process2 兩個進程都是複雜的外部依賴,不能在主體邏輯腳本里直接依賴它們,因此要把檢查進程是否存在的邏輯獨立成單獨的腳本。輸入和輸出文件的路徑依賴進程路徑,爲了測試方便,也要把獲取文件路徑的邏輯獨立成單獨的腳本。

腳本功能實現:

檢查進程是否存在和獲取進程 cwd 目錄的 util.zsh 腳本:

#!/bin/zsh

check_process() {
    pidof $1
}

get_process_cwd() {
    readlink /proc/$1/cwd
}

主體邏輯腳本 main.zsh:

#!/bin/zsh

# 有錯誤即退出,能夠省掉不少錯誤處理的代碼
set -e

# 切換到腳本當前目錄
cd ${0:h}

# 加載依賴的腳本
source ./util.zsh

# 檢查進程是否存在
local process1_pid=$(check_process process1)
local process2_pid=$(check_process process2)

# 這裏的 input 和 output 是相對腳原本說的
local input_file=$(get_process_cwd $process2_pid)/data/output.txt
local output_file=$(get_process_cwd $process1_pid)/data/input.txt

# 若是輸入文件不存在,直接退出
[[ -e $input_file ]] || {
    echo $input_file not found.
    exit 1
}

# 若是輸出文件已存在,也直接退出
[[ -e $output_file ]] && {
    echo $output_file already exists.
    exit 0
}

# 處理 $input_file 內容
# 省略

# 將結果輸出到 $output_file
# 省略

功能測試方法:

util.zsh 裏的兩個函數功能過於簡單,無需測試。

測試 main.zsh 時,須要構造一系列測試用的 util.zsh,用於模擬各類狀況:

# 進程存在的狀況
check_process() {
    echo $$
}

# 進程不存在的狀況
check_process() {
    return 1
}

# 進程 process1 存在而 process2 不存在的狀況
check_process() {
    [[ $1 == process1 ]] && echo 1234 && return
    [[ $1 == process2 ]] && return 1
}

# 輸出了進程號,但實際進程不存在的狀況
check_process() {
    echo 0
}

# 其餘狀況
# 省略


# 路徑存在的狀況
get_process_cwd() {
    [[ $1 == process1 ]] && echo /path/to/cwd1 && return
    [[ $1 == process2 ]] && echo /path/to/cwd2 && return
}

# 路徑不存在的狀況
get_process_cwd() {
    return 1
}

# 輸出了路徑,但路徑實際不存在的狀況
get_process_cwd() {
    echo /wrong/path
}

# 其餘狀況
# 省略

而後組合這些狀況,寫測試腳本判斷 main.zsh 的處理是否符合預期。

其中一個測試腳本樣例:

util_test1.zsh 內容:

#!/bin/zsh

# 進程存在
check_process() {
    echo $$
}

# 直接返回正確的路徑
get_process_cwd() {
    [[ $1 == process1 ]] && echo /path/to/cwd1 && return
    [[ $1 == process2 ]] && echo /path/to/cwd2 && return
}

test.zsh 內容:

#!/bin/zsh

# 用於測試的函數,能夠獨立成單獨腳本以便複用
assert_ok() {
    (($1 == 0)) || {
        echo Error, retcode: $1
        exit 1
    }
}

check_output_file() {
    # 檢查輸出文件是否符合預期
    # 省略
}

# 應用 util_test1.zsh
ln -sf util_test1.zsh util.zsh

# 運行腳本
./main.zsh

# 檢查返回值是否正常
assert_ok $?

# 檢查輸出文件是否符合預期
check_output_file /path/to/output/file

# 其餘檢查
# 省略

# 應用 util_test2.zsh
ln -sf util_test2.zsh util.zsh

# 省略

集成測試

測試完每一個腳本的功能後,須要將各個腳本以及其餘程序整合起來測試互相調用過程是否正常。若是功能比較複雜,須要分批整合,測試各個邏輯單元是否能正常工做。在這部分測試中,和外部環境交互的腳本若是邏輯較爲簡單,能夠不參與,用模擬腳本替代。能夠手動測試或自動測試。一樣不能只測試正常狀況。

系統測試

將全部相關組件整合起來,測試整個系統或者子系統的功能。模擬腳本不能參與系統測試,必須使用真實的外部環境。系統測試一般須要手動進行,能夠用自動化測試系統來輔助。須要覆蓋儘量多的狀況,不能只測試系統的正常功能。

總結

本文簡單介紹了 shell 腳本的測試方法,以及編寫可測試代碼的方法。

本文再也不更新,全系列文章在此更新維護:github.com/goreliu/zshguide

付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活訂價,歡迎諮詢,微信 ly50247。

相關文章
相關標籤/搜索